feat!: BiDi cookies (#11532)

Co-authored-by: Maksim Sadym <sadym@google.com>
Co-authored-by: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com>
This commit is contained in:
Maksim Sadym 2024-02-02 13:13:00 +01:00 committed by GitHub
parent 953f4207b1
commit 9cb1fde589
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 684 additions and 100 deletions

View File

@ -87,11 +87,14 @@ sidebar_label: API
| [ConnectOptions](./puppeteer.connectoptions.md) | | | [ConnectOptions](./puppeteer.connectoptions.md) | |
| [ConsoleMessageLocation](./puppeteer.consolemessagelocation.md) | | | [ConsoleMessageLocation](./puppeteer.consolemessagelocation.md) | |
| [ContinueRequestOverrides](./puppeteer.continuerequestoverrides.md) | | | [ContinueRequestOverrides](./puppeteer.continuerequestoverrides.md) | |
| [Cookie](./puppeteer.cookie.md) | Represents a cookie object. |
| [CookieParam](./puppeteer.cookieparam.md) | Cookie parameter object |
| [CoverageEntry](./puppeteer.coverageentry.md) | The CoverageEntry class represents one entry of the coverage report. | | [CoverageEntry](./puppeteer.coverageentry.md) | The CoverageEntry class represents one entry of the coverage report. |
| [Credentials](./puppeteer.credentials.md) | | | [Credentials](./puppeteer.credentials.md) | |
| [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) | Set of configurable options for CSS coverage. | | [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) | Set of configurable options for CSS coverage. |
| [CustomQueryHandler](./puppeteer.customqueryhandler.md) | | | [CustomQueryHandler](./puppeteer.customqueryhandler.md) | |
| [DebugInfo](./puppeteer.debuginfo.md) | | | [DebugInfo](./puppeteer.debuginfo.md) | |
| [DeleteCookiesRequest](./puppeteer.deletecookiesrequest.md) | |
| [Device](./puppeteer.device.md) | | | [Device](./puppeteer.device.md) | |
| [ElementScreenshotOptions](./puppeteer.elementscreenshotoptions.md) | | | [ElementScreenshotOptions](./puppeteer.elementscreenshotoptions.md) | |
| [FrameAddScriptTagOptions](./puppeteer.frameaddscripttagoptions.md) | | | [FrameAddScriptTagOptions](./puppeteer.frameaddscripttagoptions.md) | |
@ -167,45 +170,48 @@ sidebar_label: API
## Type Aliases ## Type Aliases
| Type Alias | Description | | Type Alias | Description |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ActionResult](./puppeteer.actionresult.md) | | | [ActionResult](./puppeteer.actionresult.md) | |
| [Awaitable](./puppeteer.awaitable.md) | | | [Awaitable](./puppeteer.awaitable.md) | |
| [AwaitableIterable](./puppeteer.awaitableiterable.md) | | | [AwaitableIterable](./puppeteer.awaitableiterable.md) | |
| [AwaitablePredicate](./puppeteer.awaitablepredicate.md) | | | [AwaitablePredicate](./puppeteer.awaitablepredicate.md) | |
| [AwaitedLocator](./puppeteer.awaitedlocator.md) | | | [AwaitedLocator](./puppeteer.awaitedlocator.md) | |
| [CDPEvents](./puppeteer.cdpevents.md) | | | [CDPEvents](./puppeteer.cdpevents.md) | |
| [ChromeReleaseChannel](./puppeteer.chromereleasechannel.md) | | | [ChromeReleaseChannel](./puppeteer.chromereleasechannel.md) | |
| [ConsoleMessageType](./puppeteer.consolemessagetype.md) | The supported types for console messages. | | [ConsoleMessageType](./puppeteer.consolemessagetype.md) | The supported types for console messages. |
| [ElementFor](./puppeteer.elementfor.md) | | | [CookiePriority](./puppeteer.cookiepriority.md) | Represents the cookie's 'Priority' status: https://tools.ietf.org/html/draft-west-cookie-priority-00 |
| [ErrorCode](./puppeteer.errorcode.md) | | | [CookieSameSite](./puppeteer.cookiesamesite.md) | Represents the cookie's 'SameSite' status: https://tools.ietf.org/html/draft-west-first-party-cookies |
| [EvaluateFunc](./puppeteer.evaluatefunc.md) | | | [CookieSourceScheme](./puppeteer.cookiesourcescheme.md) | Represents the source scheme of the origin that originally set the cookie. A value of "Unset" allows protocol clients to emulate legacy cookie scope for the scheme. This is a temporary ability and it will be removed in the future. |
| [EvaluateFuncWith](./puppeteer.evaluatefuncwith.md) | | | [ElementFor](./puppeteer.elementfor.md) | |
| [EventsWithWildcard](./puppeteer.eventswithwildcard.md) | | | [ErrorCode](./puppeteer.errorcode.md) | |
| [EventType](./puppeteer.eventtype.md) | | | [EvaluateFunc](./puppeteer.evaluatefunc.md) | |
| [ExperimentsConfiguration](./puppeteer.experimentsconfiguration.md) | <p>Defines experiment options for Puppeteer.</p><p>See individual properties for more information.</p> | | [EvaluateFuncWith](./puppeteer.evaluatefuncwith.md) | |
| [FlattenHandle](./puppeteer.flattenhandle.md) | | | [EventsWithWildcard](./puppeteer.eventswithwildcard.md) | |
| [HandleFor](./puppeteer.handlefor.md) | | | [EventType](./puppeteer.eventtype.md) | |
| [HandleOr](./puppeteer.handleor.md) | | | [ExperimentsConfiguration](./puppeteer.experimentsconfiguration.md) | <p>Defines experiment options for Puppeteer.</p><p>See individual properties for more information.</p> |
| [Handler](./puppeteer.handler.md) | | | [FlattenHandle](./puppeteer.flattenhandle.md) | |
| [InnerParams](./puppeteer.innerparams.md) | | | [HandleFor](./puppeteer.handlefor.md) | |
| [InterceptResolutionStrategy](./puppeteer.interceptresolutionstrategy.md) | | | [HandleOr](./puppeteer.handleor.md) | |
| [KeyInput](./puppeteer.keyinput.md) | All the valid keys that can be passed to functions that take user input, such as [keyboard.press](./puppeteer.keyboard.press.md) | | [Handler](./puppeteer.handler.md) | |
| [KeyPressOptions](./puppeteer.keypressoptions.md) | | | [InnerParams](./puppeteer.innerparams.md) | |
| [LocatorClickOptions](./puppeteer.locatorclickoptions.md) | | | [InterceptResolutionStrategy](./puppeteer.interceptresolutionstrategy.md) | |
| [LowerCasePaperFormat](./puppeteer.lowercasepaperformat.md) | | | [KeyInput](./puppeteer.keyinput.md) | All the valid keys that can be passed to functions that take user input, such as [keyboard.press](./puppeteer.keyboard.press.md) |
| [Mapper](./puppeteer.mapper.md) | | | [KeyPressOptions](./puppeteer.keypressoptions.md) | |
| [MouseButton](./puppeteer.mousebutton.md) | | | [LocatorClickOptions](./puppeteer.locatorclickoptions.md) | |
| [NodeFor](./puppeteer.nodefor.md) | | | [LowerCasePaperFormat](./puppeteer.lowercasepaperformat.md) | |
| [PaperFormat](./puppeteer.paperformat.md) | All the valid paper format types when printing a PDF. | | [Mapper](./puppeteer.mapper.md) | |
| [Permission](./puppeteer.permission.md) | | | [MouseButton](./puppeteer.mousebutton.md) | |
| [Predicate](./puppeteer.predicate.md) | | | [NodeFor](./puppeteer.nodefor.md) | |
| [Product](./puppeteer.product.md) | Supported products. | | [PaperFormat](./puppeteer.paperformat.md) | All the valid paper format types when printing a PDF. |
| [ProtocolLifeCycleEvent](./puppeteer.protocollifecycleevent.md) | | | [Permission](./puppeteer.permission.md) | |
| [ProtocolType](./puppeteer.protocoltype.md) | | | [Predicate](./puppeteer.predicate.md) | |
| [PuppeteerLifeCycleEvent](./puppeteer.puppeteerlifecycleevent.md) | | | [Product](./puppeteer.product.md) | Supported products. |
| [PuppeteerNodeLaunchOptions](./puppeteer.puppeteernodelaunchoptions.md) | Utility type exposed to enable users to define options that can be passed to <code>puppeteer.launch</code> without having to list the set of all types. | | [ProtocolLifeCycleEvent](./puppeteer.protocollifecycleevent.md) | |
| [Quad](./puppeteer.quad.md) | | | [ProtocolType](./puppeteer.protocoltype.md) | |
| [ResourceType](./puppeteer.resourcetype.md) | Resource types for HTTPRequests as perceived by the rendering engine. | | [PuppeteerLifeCycleEvent](./puppeteer.puppeteerlifecycleevent.md) | |
| [TargetFilterCallback](./puppeteer.targetfiltercallback.md) | | | [PuppeteerNodeLaunchOptions](./puppeteer.puppeteernodelaunchoptions.md) | Utility type exposed to enable users to define options that can be passed to <code>puppeteer.launch</code> without having to list the set of all types. |
| [VisibilityOption](./puppeteer.visibilityoption.md) | | | [Quad](./puppeteer.quad.md) | |
| [ResourceType](./puppeteer.resourcetype.md) | Resource types for HTTPRequests as perceived by the rendering engine. |
| [TargetFilterCallback](./puppeteer.targetfiltercallback.md) | |
| [VisibilityOption](./puppeteer.visibilityoption.md) | |

View File

@ -0,0 +1,33 @@
---
sidebar_label: Cookie
---
# Cookie interface
Represents a cookie object.
#### Signature:
```typescript
export interface Cookie
```
## Properties
| Property | Modifiers | Type | Description | Default |
| ------------------ | --------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| domain | | string | Cookie domain. | |
| expires | | number | Cookie expiration date as the number of seconds since the UNIX epoch. Set to <code>-1</code> for session cookies | |
| httpOnly | | boolean | True if cookie is http-only. | |
| name | | string | Cookie name. | |
| partitionKey | <code>optional</code> | string | Cookie partition key. The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie. Supported only in Chrome. | |
| partitionKeyOpaque | <code>optional</code> | boolean | True if cookie partition key is opaque. Supported only in Chrome. | |
| path | | string | Cookie path. | |
| priority | <code>optional</code> | [CookiePriority](./puppeteer.cookiepriority.md) | Cookie Priority. Supported only in Chrome. | |
| sameParty | <code>optional</code> | boolean | True if cookie is SameParty. Supported only in Chrome. | |
| sameSite | <code>optional</code> | [CookieSameSite](./puppeteer.cookiesamesite.md) | Cookie SameSite type. | |
| secure | | boolean | True if cookie is secure. | |
| session | | boolean | True in case of session cookie. | |
| size | | number | Cookie size. | |
| sourceScheme | <code>optional</code> | [CookieSourceScheme](./puppeteer.cookiesourcescheme.md) | Cookie source scheme type. Supported only in Chrome. | |
| value | | string | Cookie value. | |

View File

@ -0,0 +1,31 @@
---
sidebar_label: CookieParam
---
# CookieParam interface
Cookie parameter object
#### Signature:
```typescript
export interface CookieParam
```
## Properties
| Property | Modifiers | Type | Description | Default |
| ------------ | --------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| domain | <code>optional</code> | string | Cookie domain. | |
| expires | <code>optional</code> | number | Cookie expiration date, session cookie if not set | |
| httpOnly | <code>optional</code> | boolean | True if cookie is http-only. | |
| name | | string | Cookie name. | |
| partitionKey | <code>optional</code> | string | Cookie partition key. The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie. If not set, the cookie will be set as not partitioned. | |
| path | <code>optional</code> | string | Cookie path. | |
| priority | <code>optional</code> | [CookiePriority](./puppeteer.cookiepriority.md) | Cookie Priority. Supported only in Chrome. | |
| sameParty | <code>optional</code> | boolean | True if cookie is SameParty. Supported only in Chrome. | |
| sameSite | <code>optional</code> | [CookieSameSite](./puppeteer.cookiesamesite.md) | Cookie SameSite type. | |
| secure | <code>optional</code> | boolean | True if cookie is secure. | |
| sourceScheme | <code>optional</code> | [CookieSourceScheme](./puppeteer.cookiesourcescheme.md) | Cookie source scheme type. Supported only in Chrome. | |
| url | <code>optional</code> | string | The request-URI to associate with the setting of the cookie. This value can affect the default domain, path, and source scheme values of the created cookie. | |
| value | | string | Cookie value. | |

View File

@ -0,0 +1,13 @@
---
sidebar_label: CookiePriority
---
# CookiePriority type
Represents the cookie's 'Priority' status: https://tools.ietf.org/html/draft-west-cookie-priority-00
#### Signature:
```typescript
export type CookiePriority = 'Low' | 'Medium' | 'High';
```

View File

@ -0,0 +1,13 @@
---
sidebar_label: CookieSameSite
---
# CookieSameSite type
Represents the cookie's 'SameSite' status: https://tools.ietf.org/html/draft-west-first-party-cookies
#### Signature:
```typescript
export type CookieSameSite = 'Strict' | 'Lax' | 'None';
```

View File

@ -0,0 +1,13 @@
---
sidebar_label: CookieSourceScheme
---
# CookieSourceScheme type
Represents the source scheme of the origin that originally set the cookie. A value of "Unset" allows protocol clients to emulate legacy cookie scope for the scheme. This is a temporary ability and it will be removed in the future.
#### Signature:
```typescript
export type CookieSourceScheme = 'Unset' | 'NonSecure' | 'Secure';
```

View File

@ -0,0 +1,20 @@
---
sidebar_label: DeleteCookiesRequest
---
# DeleteCookiesRequest interface
#### Signature:
```typescript
export interface DeleteCookiesRequest
```
## Properties
| Property | Modifiers | Type | Description | Default |
| -------- | --------------------- | ------ | --------------------------------------------------------------------------------------------------- | ------- |
| domain | <code>optional</code> | string | If specified, deletes only cookies with the exact domain. | |
| name | | string | Name of the cookies to remove. | |
| path | <code>optional</code> | string | If specified, deletes only cookies with the exact path. | |
| url | <code>optional</code> | string | If specified, deletes all the cookies with the given name where domain and path match provided URL. | |

View File

@ -10,7 +10,7 @@ If no URLs are specified, this method returns cookies for the current page URL.
```typescript ```typescript
class Page { class Page {
abstract cookies(...urls: string[]): Promise<Protocol.Network.Cookie[]>; abstract cookies(...urls: string[]): Promise<Cookie[]>;
} }
``` ```
@ -22,4 +22,4 @@ class Page {
**Returns:** **Returns:**
Promise&lt;Protocol.Network.Cookie\[\]&gt; Promise&lt;[Cookie](./puppeteer.cookie.md)\[\]&gt;

View File

@ -8,17 +8,15 @@ sidebar_label: Page.deleteCookie
```typescript ```typescript
class Page { class Page {
abstract deleteCookie( abstract deleteCookie(...cookies: DeleteCookiesRequest[]): Promise<void>;
...cookies: Protocol.Network.DeleteCookiesRequest[]
): Promise<void>;
} }
``` ```
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | ----------------------------------------- | ----------- | | --------- | --------------------------------------------------------------- | ----------- |
| cookies | Protocol.Network.DeleteCookiesRequest\[\] | | | cookies | [DeleteCookiesRequest](./puppeteer.deletecookiesrequest.md)\[\] | |
**Returns:** **Returns:**

View File

@ -8,15 +8,15 @@ sidebar_label: Page.setCookie
```typescript ```typescript
class Page { class Page {
abstract setCookie(...cookies: Protocol.Network.CookieParam[]): Promise<void>; abstract setCookie(...cookies: CookieParam[]): Promise<void>;
} }
``` ```
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | -------------------------------- | ----------- | | --------- | --------------------------------------------- | ----------- |
| cookies | Protocol.Network.CookieParam\[\] | | | cookies | [CookieParam](./puppeteer.cookieparam.md)\[\] | |
**Returns:** **Returns:**

View File

@ -38,6 +38,11 @@ import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js';
import type {Credentials, NetworkConditions} from '../cdp/NetworkManager.js'; import type {Credentials, NetworkConditions} from '../cdp/NetworkManager.js';
import type {Tracing} from '../cdp/Tracing.js'; import type {Tracing} from '../cdp/Tracing.js';
import type {ConsoleMessage} from '../common/ConsoleMessage.js'; import type {ConsoleMessage} from '../common/ConsoleMessage.js';
import type {
Cookie,
CookieParam,
DeleteCookiesRequest,
} from '../common/Cookie.js';
import type {Device} from '../common/Device.js'; import type {Device} from '../common/Device.js';
import {TargetCloseError} from '../common/Errors.js'; import {TargetCloseError} from '../common/Errors.js';
import { import {
@ -1303,11 +1308,9 @@ export abstract class Page extends EventEmitter<PageEvents> {
* If no URLs are specified, this method returns cookies for the current page * If no URLs are specified, this method returns cookies for the current page
* URL. If URLs are specified, only cookies for those URLs are returned. * URL. If URLs are specified, only cookies for those URLs are returned.
*/ */
abstract cookies(...urls: string[]): Promise<Protocol.Network.Cookie[]>; abstract cookies(...urls: string[]): Promise<Cookie[]>;
abstract deleteCookie( abstract deleteCookie(...cookies: DeleteCookiesRequest[]): Promise<void>;
...cookies: Protocol.Network.DeleteCookiesRequest[]
): Promise<void>;
/** /**
* @example * @example
@ -1316,7 +1319,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
* await page.setCookie(cookieObject1, cookieObject2); * await page.setCookie(cookieObject1, cookieObject2);
* ``` * ```
*/ */
abstract setCookie(...cookies: Protocol.Network.CookieParam[]): Promise<void>; abstract setCookie(...cookies: CookieParam[]): Promise<void>;
/** /**
* Adds a `<script>` tag into the page with the desired URL or content. * Adds a `<script>` tag into the page with the desired URL or content.

View File

@ -6,7 +6,7 @@
import type {Readable} from 'stream'; import type {Readable} from 'stream';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol'; import type Protocol from 'devtools-protocol';
import { import {
@ -37,6 +37,7 @@ import {
ConsoleMessage, ConsoleMessage,
type ConsoleMessageLocation, type ConsoleMessageLocation,
} from '../common/ConsoleMessage.js'; } from '../common/ConsoleMessage.js';
import type {Cookie, CookieSameSite, CookieParam} from '../common/Cookie.js';
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js'; import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js'; import type {Handler} from '../common/EventEmitter.js';
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js'; import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
@ -774,6 +775,28 @@ export class BidiPage extends Page {
}); });
} }
override async cookies(...urls: string[]): Promise<Cookie[]> {
const normalizedUrls = (urls.length ? urls : [this.url()]).map(url => {
return new URL(url);
});
const bidiCookies = await this.#connection.send('storage.getCookies', {
partition: {
type: 'context',
context: this.mainFrame()._id,
},
});
return bidiCookies.result.cookies
.map(cookie => {
return bidiToPuppeteerCookie(cookie);
})
.filter(cookie => {
return normalizedUrls.some(url => {
return testUrlMatchCookie(cookie, url);
});
});
}
override isServiceWorkerBypassed(): never { override isServiceWorkerBypassed(): never {
throw new UnsupportedOperation(); throw new UnsupportedOperation();
} }
@ -810,12 +833,77 @@ export class BidiPage extends Page {
throw new UnsupportedOperation(); throw new UnsupportedOperation();
} }
override cookies(): never { override async setCookie(...cookies: CookieParam[]): Promise<void> {
throw new UnsupportedOperation(); const pageURL = this.url();
} const pageUrlStartsWithHTTP = pageURL.startsWith('http');
for (const cookie of cookies) {
let cookieUrl = cookie.url || '';
if (!cookieUrl && pageUrlStartsWithHTTP) {
cookieUrl = pageURL;
}
assert(
cookieUrl !== 'about:blank',
`Blank page can not have cookie "${cookie.name}"`
);
assert(
!String.prototype.startsWith.call(cookieUrl || '', 'data:'),
`Data URL page can not have cookie "${cookie.name}"`
);
override setCookie(): never { const normalizedUrl = URL.canParse(cookieUrl)
throw new UnsupportedOperation(); ? new URL(cookieUrl)
: undefined;
const domain = cookie.domain ?? normalizedUrl?.hostname;
assert(
domain !== undefined,
`At least one of the url and domain needs to be specified`
);
const bidiCookie: Bidi.Storage.PartialCookie = {
domain: domain,
name: cookie.name,
value: {
type: 'string',
value: cookie.value,
},
...(cookie.path !== undefined ? {path: cookie.path} : {}),
...(cookie.httpOnly !== undefined ? {httpOnly: cookie.httpOnly} : {}),
...(cookie.secure !== undefined ? {secure: cookie.secure} : {}),
...(cookie.sameSite !== undefined
? {sameSite: convertCookiesSameSiteCdpToBiDi(cookie.sameSite)}
: {}),
...(cookie.expires !== undefined ? {expiry: cookie.expires} : {}),
// Chrome-specific properties.
...cdpSpecificCookiePropertiesFromPuppeteerToBidi(
cookie,
'sameParty',
'sourceScheme',
'priority',
'url'
),
};
// TODO: delete cookie before setting them.
// await this.deleteCookie(bidiCookie);
const partition: Bidi.Storage.PartitionDescriptor =
cookie.partitionKey !== undefined
? {
type: 'storageKey',
sourceOrigin: cookie.partitionKey,
userContext: this.#browserContext.id,
}
: {
type: 'context',
context: this.mainFrame()._id,
};
await this.#connection.send('storage.setCookie', {
cookie: bidiCookie,
partition,
});
}
} }
override deleteCookie(): never { override deleteCookie(): never {
@ -911,3 +999,134 @@ function getStackTraceLocations(
function evaluationExpression(fun: Function | string, ...args: unknown[]) { function evaluationExpression(fun: Function | string, ...args: unknown[]) {
return `() => {${evaluationString(fun, ...args)}}`; return `() => {${evaluationString(fun, ...args)}}`;
} }
/**
* Check domains match.
* According to cookies spec, this check should match subdomains as well, but CDP
* implementation does not do that, so this method matches only the exact domains, not
* what is written in the spec:
* https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
*/
function testUrlMatchCookieHostname(
cookie: Cookie,
normalizedUrl: URL
): boolean {
const cookieDomain = cookie.domain.toLowerCase();
const urlHostname = normalizedUrl.hostname.toLowerCase();
return cookieDomain === urlHostname;
}
/**
* Check paths match.
* Spec: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
*/
function testUrlMatchCookiePath(cookie: Cookie, normalizedUrl: URL): boolean {
const uriPath = normalizedUrl.pathname;
const cookiePath = cookie.path;
if (uriPath === cookiePath) {
// The cookie-path and the request-path are identical.
return true;
}
if (uriPath.startsWith(cookiePath)) {
// The cookie-path is a prefix of the request-path.
if (cookiePath.endsWith('/')) {
// The last character of the cookie-path is %x2F ("/").
return true;
}
if (uriPath[cookiePath.length] === '/') {
// The first character of the request-path that is not included in the cookie-path
// is a %x2F ("/") character.
return true;
}
}
return false;
}
/**
* Checks the cookie matches the URL according to the spec:
*/
function testUrlMatchCookie(cookie: Cookie, url: URL): boolean {
const normalizedUrl = new URL(url);
assert(cookie !== undefined);
if (!testUrlMatchCookieHostname(cookie, normalizedUrl)) {
return false;
}
return testUrlMatchCookiePath(cookie, normalizedUrl);
}
function bidiToPuppeteerCookie(bidiCookie: Bidi.Network.Cookie): Cookie {
return {
name: bidiCookie.name,
// Presents binary value as base64 string.
value: bidiCookie.value.value,
domain: bidiCookie.domain,
path: bidiCookie.path,
size: bidiCookie.size,
httpOnly: bidiCookie.httpOnly,
secure: bidiCookie.secure,
sameSite: convertCookiesSameSiteBiDiToCdp(bidiCookie.sameSite),
expires: bidiCookie.expiry ?? -1,
session: bidiCookie.expiry === undefined || bidiCookie.expiry <= 0,
// Extending with CDP-specific properties with `goog:` prefix.
...cdpSpecificCookiePropertiesFromBidiToPuppeteer(
bidiCookie,
'sameParty',
'sourceScheme',
'partitionKey',
'partitionKeyOpaque',
'priority'
),
};
}
const CDP_SPECIFIC_PREFIX = 'goog:';
/**
* Gets CDP-specific properties from the BiDi cookie and returns them as a new object.
*/
function cdpSpecificCookiePropertiesFromBidiToPuppeteer(
bidiCookie: Bidi.Network.Cookie,
...propertyNames: Array<keyof Cookie>
): Partial<Cookie> {
const result: Partial<Cookie> = {};
for (const property of propertyNames) {
if (bidiCookie[CDP_SPECIFIC_PREFIX + property] !== undefined) {
result[property] = bidiCookie[CDP_SPECIFIC_PREFIX + property];
}
}
return result;
}
/**
* Gets CDP-specific properties from the cookie, adds CDP-specific prefixes and returns
* them as a new object which can be used in BiDi.
*/
function cdpSpecificCookiePropertiesFromPuppeteerToBidi(
cookieParam: CookieParam,
...propertyNames: Array<keyof CookieParam>
): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const property of propertyNames) {
if (cookieParam[property] !== undefined) {
result[CDP_SPECIFIC_PREFIX + property] = cookieParam[property];
}
}
return result;
}
function convertCookiesSameSiteBiDiToCdp(
sameSite: Bidi.Network.SameSite | undefined
): CookieSameSite {
return sameSite === 'strict' ? 'Strict' : sameSite === 'lax' ? 'Lax' : 'None';
}
function convertCookiesSameSiteCdpToBiDi(
sameSite: CookieSameSite | undefined
): Bidi.Network.SameSite {
return sameSite === 'Strict'
? Bidi.Network.SameSite.Strict
: sameSite === 'Lax'
? Bidi.Network.SameSite.Lax
: Bidi.Network.SameSite.None;
}

View File

@ -127,6 +127,15 @@ export interface Commands {
params: Bidi.Session.SubscriptionRequest; params: Bidi.Session.SubscriptionRequest;
returnType: Bidi.EmptyResult; returnType: Bidi.EmptyResult;
}; };
'storage.getCookies': {
params: Bidi.Storage.GetCookiesParameters;
returnType: Bidi.Storage.GetCookiesResult;
};
'storage.setCookie': {
params: Bidi.Storage.SetCookieParameters;
returnType: Bidi.Storage.SetCookieParameters;
};
} }
/** /**

View File

@ -32,6 +32,11 @@ import {
ConsoleMessage, ConsoleMessage,
type ConsoleMessageType, type ConsoleMessageType,
} from '../common/ConsoleMessage.js'; } from '../common/ConsoleMessage.js';
import type {
Cookie,
DeleteCookiesRequest,
CookieParam,
} from '../common/Cookie.js';
import {TargetCloseError} from '../common/Errors.js'; import {TargetCloseError} from '../common/Errors.js';
import {FileChooser} from '../common/FileChooser.js'; import {FileChooser} from '../common/FileChooser.js';
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js'; import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
@ -572,16 +577,14 @@ export class CdpPage extends Page {
) as HandleFor<Prototype[]>; ) as HandleFor<Prototype[]>;
} }
override async cookies( override async cookies(...urls: string[]): Promise<Cookie[]> {
...urls: string[]
): Promise<Protocol.Network.Cookie[]> {
const originalCookies = ( const originalCookies = (
await this.#primaryTargetClient.send('Network.getCookies', { await this.#primaryTargetClient.send('Network.getCookies', {
urls: urls.length ? urls : [this.url()], urls: urls.length ? urls : [this.url()],
}) })
).cookies; ).cookies;
const unsupportedCookieAttributes = ['priority']; const unsupportedCookieAttributes = ['sourcePort'];
const filterUnsupportedAttributes = ( const filterUnsupportedAttributes = (
cookie: Protocol.Network.Cookie cookie: Protocol.Network.Cookie
): Protocol.Network.Cookie => { ): Protocol.Network.Cookie => {
@ -594,7 +597,7 @@ export class CdpPage extends Page {
} }
override async deleteCookie( override async deleteCookie(
...cookies: Protocol.Network.DeleteCookiesRequest[] ...cookies: DeleteCookiesRequest[]
): Promise<void> { ): Promise<void> {
const pageURL = this.url(); const pageURL = this.url();
for (const cookie of cookies) { for (const cookie of cookies) {
@ -606,9 +609,7 @@ export class CdpPage extends Page {
} }
} }
override async setCookie( override async setCookie(...cookies: CookieParam[]): Promise<void> {
...cookies: Protocol.Network.CookieParam[]
): Promise<void> {
const pageURL = this.url(); const pageURL = this.url();
const startsWithHTTP = pageURL.startsWith('http'); const startsWithHTTP = pageURL.startsWith('http');
const items = cookies.map(cookie => { const items = cookies.map(cookie => {

View File

@ -0,0 +1,186 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Represents the cookie's 'SameSite' status:
* https://tools.ietf.org/html/draft-west-first-party-cookies
*
* @public
*/
export type CookieSameSite = 'Strict' | 'Lax' | 'None';
/**
* Represents the cookie's 'Priority' status:
* https://tools.ietf.org/html/draft-west-cookie-priority-00
*
* @public
*/
export type CookiePriority = 'Low' | 'Medium' | 'High';
/**
* Represents the source scheme of the origin that originally set the cookie. A value of
* "Unset" allows protocol clients to emulate legacy cookie scope for the scheme.
* This is a temporary ability and it will be removed in the future.
*
* @public
*/
export type CookieSourceScheme = 'Unset' | 'NonSecure' | 'Secure';
/**
* Represents a cookie object.
*
* @public
*/
export interface Cookie {
/**
* Cookie name.
*/
name: string;
/**
* Cookie value.
*/
value: string;
/**
* Cookie domain.
*/
domain: string;
/**
* Cookie path.
*/
path: string;
/**
* Cookie expiration date as the number of seconds since the UNIX epoch. Set to `-1` for
* session cookies
*/
expires: number;
/**
* Cookie size.
*/
size: number;
/**
* True if cookie is http-only.
*/
httpOnly: boolean;
/**
* True if cookie is secure.
*/
secure: boolean;
/**
* True in case of session cookie.
*/
session: boolean;
/**
* Cookie SameSite type.
*/
sameSite?: CookieSameSite;
/**
* Cookie Priority. Supported only in Chrome.
*/
priority?: CookiePriority;
/**
* True if cookie is SameParty. Supported only in Chrome.
*/
sameParty?: boolean;
/**
* Cookie source scheme type. Supported only in Chrome.
*/
sourceScheme?: CookieSourceScheme;
/**
* Cookie partition key. The site of the top-level URL the browser was visiting at the
* start of the request to the endpoint that set the cookie. Supported only in Chrome.
*/
partitionKey?: string;
/**
* True if cookie partition key is opaque. Supported only in Chrome.
*/
partitionKeyOpaque?: boolean;
}
/**
* Cookie parameter object
*
* @public
*/
export interface CookieParam {
/**
* Cookie name.
*/
name: string;
/**
* Cookie value.
*/
value: string;
/**
* The request-URI to associate with the setting of the cookie. This value can affect
* the default domain, path, and source scheme values of the created cookie.
*/
url?: string;
/**
* Cookie domain.
*/
domain?: string;
/**
* Cookie path.
*/
path?: string;
/**
* True if cookie is secure.
*/
secure?: boolean;
/**
* True if cookie is http-only.
*/
httpOnly?: boolean;
/**
* Cookie SameSite type.
*/
sameSite?: CookieSameSite;
/**
* Cookie expiration date, session cookie if not set
*/
expires?: number;
/**
* Cookie Priority. Supported only in Chrome.
*/
priority?: CookiePriority;
/**
* True if cookie is SameParty. Supported only in Chrome.
*/
sameParty?: boolean;
/**
* Cookie source scheme type. Supported only in Chrome.
*/
sourceScheme?: CookieSourceScheme;
/**
* Cookie partition key. The site of the top-level URL the browser was visiting at the
* start of the request to the endpoint that set the cookie. If not set, the cookie will
* be set as not partitioned.
*/
partitionKey?: string;
}
/**
* @public
*/
export interface DeleteCookiesRequest {
/**
* Name of the cookies to remove.
*/
name: string;
/**
* If specified, deletes all the cookies with the given name where domain and path match
* provided URL.
*/
url?: string;
/**
* If specified, deletes only cookies with the exact domain.
*/
domain?: string;
/**
* If specified, deletes only cookies with the exact path.
*/
path?: string;
}

View File

@ -10,6 +10,7 @@ export * from './Configuration.js';
export * from './ConnectionTransport.js'; export * from './ConnectionTransport.js';
export * from './ConnectOptions.js'; export * from './ConnectOptions.js';
export * from './ConsoleMessage.js'; export * from './ConsoleMessage.js';
export * from './Cookie.js';
export * from './CustomQueryHandler.js'; export * from './CustomQueryHandler.js';
export * from './Debug.js'; export * from './Debug.js';
export * from './Device.js'; export * from './Device.js';

View File

@ -443,6 +443,12 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[cookies.spec] Cookie specs *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[coverage.spec] *", "testIdPattern": "[coverage.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -1584,10 +1590,28 @@
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{ {
"testIdPattern": "[cookies.spec] Cookie specs Page.cookies should get cookies from multiple urls", "testIdPattern": "[cookies.spec] Cookie specs Page.cookies should get cookies from nested path",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["PASS"] "expectations": ["FAIL"]
},
{
"testIdPattern": "[cookies.spec] Cookie specs Page.cookies should not get cookies from not nested path",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[cookies.spec] Cookie specs Page.cookies should not get cookies from subdomain",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[cookies.spec] Cookie specs Page.deleteCookie should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL"]
}, },
{ {
"testIdPattern": "[cookies.spec] Cookie specs Page.deleteCookie should work", "testIdPattern": "[cookies.spec] Cookie specs Page.deleteCookie should work",
@ -1595,12 +1619,6 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should default to setting secure cookie for HTTPS websites",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should isolate cookies in browser contexts", "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should isolate cookies in browser contexts",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -1637,12 +1655,6 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set secure same-site cookies from a frame",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should work", "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should work",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],

View File

@ -152,7 +152,6 @@ describe('Cookie specs', () => {
httpOnly: false, httpOnly: false,
secure: true, secure: true,
session: true, session: true,
sourcePort: 443,
sourceScheme: 'Secure', sourceScheme: 'Secure',
}, },
{ {
@ -166,11 +165,46 @@ describe('Cookie specs', () => {
httpOnly: false, httpOnly: false,
secure: true, secure: true,
session: true, session: true,
sourcePort: 443,
sourceScheme: 'Secure', sourceScheme: 'Secure',
}, },
]); ]);
}); });
it('should not get cookies from subdomain', async () => {
const {page} = await getTestState();
await page.setCookie({
url: 'https://base_domain.com',
name: 'doggo',
value: 'woofs',
});
const cookies = await page.cookies('https://sub_domain.base_domain.com');
expect(cookies).toHaveLength(0);
});
it('should get cookies from nested path', async () => {
const {page} = await getTestState();
await page.setCookie({
url: 'https://foo.com',
path: '/some_path',
name: 'doggo',
value: 'woofs',
});
const cookies = await page.cookies(
'https://foo.com/some_path/nested_path'
);
expect(cookies).toHaveLength(1);
});
it('should not get cookies from not nested path', async () => {
const {page} = await getTestState();
await page.setCookie({
url: 'https://foo.com',
path: '/some_path',
name: 'doggo',
value: 'woofs',
});
const cookies = await page.cookies(
'https://foo.com/some_path_looks_like_nested'
);
expect(cookies).toHaveLength(0);
});
}); });
describe('Page.setCookie', function () { describe('Page.setCookie', function () {
it('should work', async () => { it('should work', async () => {
@ -271,7 +305,6 @@ describe('Cookie specs', () => {
httpOnly: false, httpOnly: false,
secure: false, secure: false,
session: true, session: true,
sourcePort: 80,
sourceScheme: 'NonSecure', sourceScheme: 'NonSecure',
}, },
] ]
@ -298,7 +331,6 @@ describe('Cookie specs', () => {
httpOnly: false, httpOnly: false,
secure: false, secure: false,
session: true, session: true,
sourcePort: 80,
sourceScheme: 'NonSecure', sourceScheme: 'NonSecure',
}, },
]); ]);
@ -403,7 +435,6 @@ describe('Cookie specs', () => {
httpOnly: false, httpOnly: false,
secure: true, secure: true,
session: true, session: true,
sourcePort: 443,
sourceScheme: 'Secure', sourceScheme: 'Secure',
}, },
]); ]);
@ -446,7 +477,6 @@ describe('Cookie specs', () => {
httpOnly: false, httpOnly: false,
secure: false, secure: false,
session: true, session: true,
sourcePort: 80,
sourceScheme: 'NonSecure', sourceScheme: 'NonSecure',
}, },
]); ]);
@ -465,7 +495,6 @@ describe('Cookie specs', () => {
httpOnly: false, httpOnly: false,
secure: false, secure: false,
session: true, session: true,
sourcePort: 80,
sourceScheme: 'NonSecure', sourceScheme: 'NonSecure',
}, },
] ]
@ -515,7 +544,6 @@ describe('Cookie specs', () => {
sameSite: 'None', sameSite: 'None',
secure: true, secure: true,
session: true, session: true,
sourcePort: 443,
sourceScheme: 'Secure', sourceScheme: 'Secure',
}, },
] ]

View File

@ -62,7 +62,6 @@ describe('DefaultBrowserContext', function () {
httpOnly: false, httpOnly: false,
secure: false, secure: false,
session: true, session: true,
sourcePort: 80,
sourceScheme: 'NonSecure', sourceScheme: 'NonSecure',
}, },
]); ]);
@ -96,7 +95,6 @@ describe('DefaultBrowserContext', function () {
httpOnly: false, httpOnly: false,
secure: false, secure: false,
session: true, session: true,
sourcePort: 80,
sourceScheme: 'NonSecure', sourceScheme: 'NonSecure',
}, },
]); ]);

View File

@ -8,13 +8,13 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import {TestServer} from '@pptr/testserver'; import {TestServer} from '@pptr/testserver';
import type {Protocol} from 'devtools-protocol';
import expect from 'expect'; import expect from 'expect';
import type * as MochaBase from 'mocha'; import type * as MochaBase from 'mocha';
import puppeteer from 'puppeteer/lib/cjs/puppeteer/puppeteer.js'; import puppeteer from 'puppeteer/lib/cjs/puppeteer/puppeteer.js';
import type {Browser} from 'puppeteer-core/internal/api/Browser.js'; import type {Browser} from 'puppeteer-core/internal/api/Browser.js';
import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js';
import type {Page} from 'puppeteer-core/internal/api/Page.js'; import type {Page} from 'puppeteer-core/internal/api/Page.js';
import type {Cookie} from 'puppeteer-core/internal/common/Cookie.js';
import type { import type {
PuppeteerLaunchOptions, PuppeteerLaunchOptions,
PuppeteerNode, PuppeteerNode,
@ -372,8 +372,8 @@ expect.extend({
}); });
export const expectCookieEquals = async ( export const expectCookieEquals = async (
cookies: Protocol.Network.Cookie[], cookies: Cookie[],
expectedCookies: Array<Partial<Protocol.Network.Cookie>> expectedCookies: Array<Partial<Cookie>>
): Promise<void> => { ): Promise<void> => {
if (!processVariables.isChrome) { if (!processVariables.isChrome) {
// Only keep standard properties when testing on a browser other than Chrome. // Only keep standard properties when testing on a browser other than Chrome.