chrome-devtools-frontend
Version:
Chrome DevTools UI
320 lines (270 loc) • 9.86 kB
text/typescript
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as Protocol from '../../generated/protocol.js';
import type * as Platform from '../platform/platform.js';
const OPAQUE_PARTITION_KEY = '<opaque>';
export class Cookie {
readonly #name: string;
readonly #value: string;
readonly #type: Type|null|undefined;
#attributes = new Map<Attribute, string|number|boolean|undefined>();
#size = 0;
#priority: Protocol.Network.CookiePriority;
#cookieLine: string|null = null;
#partitionKey: Protocol.Network.CookiePartitionKey|undefined;
constructor(
name: string, value: string, type?: Type|null, priority?: Protocol.Network.CookiePriority,
partitionKey?: Protocol.Network.CookiePartitionKey) {
this.#name = name;
this.#value = value;
this.#type = type;
this.#priority = (priority || 'Medium' as Protocol.Network.CookiePriority);
this.#partitionKey = partitionKey;
}
static fromProtocolCookie(protocolCookie: Protocol.Network.Cookie): Cookie {
const cookie = new Cookie(protocolCookie.name, protocolCookie.value, null, protocolCookie.priority);
cookie.addAttribute(Attribute.DOMAIN, protocolCookie['domain']);
cookie.addAttribute(Attribute.PATH, protocolCookie['path']);
if (protocolCookie['expires']) {
cookie.addAttribute(Attribute.EXPIRES, protocolCookie['expires'] * 1000);
}
if (protocolCookie['httpOnly']) {
cookie.addAttribute(Attribute.HTTP_ONLY);
}
if (protocolCookie['secure']) {
cookie.addAttribute(Attribute.SECURE);
}
if (protocolCookie['sameSite']) {
cookie.addAttribute(Attribute.SAME_SITE, protocolCookie['sameSite']);
}
if ('sourcePort' in protocolCookie) {
cookie.addAttribute(Attribute.SOURCE_PORT, protocolCookie.sourcePort);
}
if ('sourceScheme' in protocolCookie) {
cookie.addAttribute(Attribute.SOURCE_SCHEME, protocolCookie.sourceScheme);
}
if ('partitionKey' in protocolCookie) {
if (protocolCookie.partitionKey) {
cookie.setPartitionKey(
protocolCookie.partitionKey.topLevelSite, protocolCookie.partitionKey.hasCrossSiteAncestor);
}
}
if ('partitionKeyOpaque' in protocolCookie && protocolCookie.partitionKeyOpaque) {
cookie.addAttribute(Attribute.PARTITION_KEY, OPAQUE_PARTITION_KEY);
}
cookie.setSize(protocolCookie['size']);
return cookie;
}
key(): string {
return (this.domain() || '-') + ' ' + this.name() + ' ' + (this.path() || '-') + ' ' +
(this.partitionKey() ?
(this.topLevelSite() + ' ' + (this.hasCrossSiteAncestor() ? 'cross_site' : 'same_site')) :
'-');
}
name(): string {
return this.#name;
}
value(): string {
return this.#value;
}
type(): Type|null|undefined {
return this.#type;
}
httpOnly(): boolean {
return this.#attributes.has(Attribute.HTTP_ONLY);
}
secure(): boolean {
return this.#attributes.has(Attribute.SECURE);
}
partitioned(): boolean {
return this.#attributes.has(Attribute.PARTITIONED) || Boolean(this.partitionKey()) || this.partitionKeyOpaque();
}
sameSite(): Protocol.Network.CookieSameSite {
// TODO(allada) This should not rely on #attributes and instead store them individually.
// when #attributes get added via addAttribute() they are lowercased, hence the lowercasing of samesite here
return this.#attributes.get(Attribute.SAME_SITE) as Protocol.Network.CookieSameSite;
}
partitionKey(): Protocol.Network.CookiePartitionKey {
return this.#partitionKey as Protocol.Network.CookiePartitionKey;
}
setPartitionKey(topLevelSite: string, hasCrossSiteAncestor: boolean): void {
this.#partitionKey = {topLevelSite, hasCrossSiteAncestor};
if (!this.#attributes.has(Attribute.PARTITIONED)) {
this.addAttribute(Attribute.PARTITIONED);
}
}
topLevelSite(): string {
if (!this.#partitionKey) {
return '';
}
return this.#partitionKey?.topLevelSite;
}
setTopLevelSite(topLevelSite: string, hasCrossSiteAncestor: boolean): void {
this.setPartitionKey(topLevelSite, hasCrossSiteAncestor);
}
hasCrossSiteAncestor(): boolean {
if (!this.#partitionKey) {
return false;
}
return this.#partitionKey?.hasCrossSiteAncestor;
}
setHasCrossSiteAncestor(hasCrossSiteAncestor: boolean): void {
if (!this.partitionKey() || !Boolean(this.topLevelSite())) {
return;
}
this.setPartitionKey(this.topLevelSite(), hasCrossSiteAncestor);
}
partitionKeyOpaque(): boolean {
if (!this.#partitionKey) {
return false;
}
return (this.topLevelSite() === OPAQUE_PARTITION_KEY);
}
setPartitionKeyOpaque(): void {
this.addAttribute(Attribute.PARTITION_KEY, OPAQUE_PARTITION_KEY);
this.setPartitionKey(OPAQUE_PARTITION_KEY, false);
}
priority(): Protocol.Network.CookiePriority {
return this.#priority;
}
session(): boolean {
// RFC 2965 suggests using Discard attribute to mark session cookies, but this does not seem to be widely used.
// Check for absence of explicitly max-age or expiry date instead.
return !(this.#attributes.has(Attribute.EXPIRES) || this.#attributes.has(Attribute.MAX_AGE));
}
path(): string {
return this.#attributes.get(Attribute.PATH) as string;
}
domain(): string {
return this.#attributes.get(Attribute.DOMAIN) as string;
}
expires(): number {
return this.#attributes.get(Attribute.EXPIRES) as number;
}
maxAge(): number {
return this.#attributes.get(Attribute.MAX_AGE) as number;
}
sourcePort(): number {
return this.#attributes.get(Attribute.SOURCE_PORT) as number;
}
sourceScheme(): Protocol.Network.CookieSourceScheme {
return this.#attributes.get(Attribute.SOURCE_SCHEME) as Protocol.Network.CookieSourceScheme;
}
size(): number {
return this.#size;
}
/**
* @deprecated
*/
url(): Platform.DevToolsPath.UrlString|null {
if (!this.domain() || !this.path()) {
return null;
}
let port = '';
const sourcePort = this.sourcePort();
// Do not include standard ports to ensure the back-end will change standard ports according to the scheme.
if (sourcePort && sourcePort !== 80 && sourcePort !== 443) {
port = `:${this.sourcePort()}`;
}
// We must not consider the this.sourceScheme() here, otherwise it will be impossible to set a cookie without
// the Secure attribute from a secure origin.
return (this.secure() ? 'https://' : 'http://') + this.domain() + port + this.path() as
Platform.DevToolsPath.UrlString;
}
setSize(size: number): void {
this.#size = size;
}
expiresDate(requestDate: Date): Date|null {
// RFC 6265 indicates that the max-age attribute takes precedence over the expires attribute
if (this.maxAge()) {
return new Date(requestDate.getTime() + 1000 * this.maxAge());
}
if (this.expires()) {
return new Date(this.expires());
}
return null;
}
addAttribute(key: Attribute|null, value?: string|number|boolean): void {
if (!key) {
return;
}
switch (key) {
case Attribute.PRIORITY:
this.#priority = (value as Protocol.Network.CookiePriority);
break;
default:
this.#attributes.set(key, value);
}
}
hasAttribute(key: Attribute): boolean {
return this.#attributes.has(key);
}
getAttribute(key: Attribute): string|number|boolean|undefined {
return this.#attributes.get(key);
}
setCookieLine(cookieLine: string): void {
this.#cookieLine = cookieLine;
}
getCookieLine(): string|null {
return this.#cookieLine;
}
matchesSecurityOrigin(securityOrigin: string): boolean {
const hostname = new URL(securityOrigin).hostname;
return Cookie.isDomainMatch(this.domain(), hostname);
}
static isDomainMatch(domain: string, hostname: string): boolean {
// This implementation mirrors
// https://source.chromium.org/search?q=net::cookie_util::IsDomainMatch()
//
// Can domain match in two ways; as a domain cookie (where the cookie
// domain begins with ".") or as a host cookie (where it doesn't).
// Some consumers of the CookieMonster expect to set cookies on
// URLs like http://.strange.url. To retrieve cookies in this instance,
// we allow matching as a host cookie even when the domain_ starts with
// a period.
if (hostname === domain) {
return true;
}
// Domain cookie must have an initial ".". To match, it must be
// equal to url's host with initial period removed, or a suffix of
// it.
// Arguably this should only apply to "http" or "https" cookies, but
// extension cookie tests currently use the funtionality, and if we
// ever decide to implement that it should be done by preventing
// such cookies from being set.
if (domain?.[0] !== '.') {
return false;
}
// The host with a "." prefixed.
if (domain.substr(1) === hostname) {
return true;
}
// A pure suffix of the host (ok since we know the domain already
// starts with a ".")
return hostname.length > domain.length && hostname.endsWith(domain);
}
}
export const enum Type {
REQUEST = 0,
RESPONSE = 1,
}
export const enum Attribute {
NAME = 'name',
VALUE = 'value',
SIZE = 'size',
DOMAIN = 'domain',
PATH = 'path',
EXPIRES = 'expires',
MAX_AGE = 'max-age',
HTTP_ONLY = 'http-only',
SECURE = 'secure',
SAME_SITE = 'same-site',
SOURCE_SCHEME = 'source-scheme',
SOURCE_PORT = 'source-port',
PRIORITY = 'priority',
PARTITIONED = 'partitioned',
PARTITION_KEY = 'partition-key',
PARTITION_KEY_SITE = 'partition-key-site',
HAS_CROSS_SITE_ANCESTOR = 'has-cross-site-ancestor',
}