Skip to content

Commit

Permalink
Merge pull request #667 from Mas0nShi/666-fixes-cookie-jar
Browse files Browse the repository at this point in the history
#666@patch: Fixes Cookie Manager.
  • Loading branch information
capricorn86 committed Dec 3, 2022
2 parents b9e3a88 + 0544614 commit b9ecfc7
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 91 deletions.
158 changes: 158 additions & 0 deletions packages/happy-dom/src/cookie/Cookie.ts
@@ -0,0 +1,158 @@
const CookiePairRegex = /([^=]+)(?:=([\s\S]*))?/;

/**
* Cookie.
*/
export default class Cookie {
private pairs: { [key: string]: string } = {};
//
public key = '';
public value = '';
public size = 0;
// Optional
public domain = '';
public path = '';
public expriesOrMaxAge: Date = null;
public httpOnly = false;
public secure = false;
public sameSite = '';

/**
* Constructor.
*
* @param cookie Cookie.
*/
constructor(cookie: string) {
let match: RegExpExecArray | null;

const parts = cookie.split(';').filter(Boolean);

// Part[0] is the key-value pair.
match = new RegExp(CookiePairRegex).exec(parts[0]);
if (!match) {
throw new Error(`Invalid cookie: ${cookie}`);
}
this.key = match[1].trim();
this.value = match[2];
// Set key is empty if match[2] is undefined.
if (!match[2]) {
this.value = this.key;
this.key = '';
}
this.pairs[this.key] = this.value;
this.size = this.key.length + this.value.length;
// Attribute.
for (const part of parts.slice(1)) {
match = new RegExp(CookiePairRegex).exec(part);
if (!match) {
throw new Error(`Invalid cookie: ${part}`);
}
const key = match[1].trim();
const value = match[2];

switch (key.toLowerCase()) {
case 'expires':
this.expriesOrMaxAge = new Date(value);
break;
case 'max-age':
this.expriesOrMaxAge = new Date(parseInt(value, 10) * 1000 + Date.now());
break;
case 'domain':
this.domain = value;
break;
case 'path':
this.path = value.startsWith('/') ? value : `/${value}`;
break;
case 'httponly':
this.httpOnly = true;
break;
case 'secure':
this.secure = true;
break;
case 'samesite':
this.sameSite = value;
break;
default:
continue; // Skip.
}
// Skip unknown key-value pair.
if (
['expires', 'max-age', 'domain', 'path', 'httponly', 'secure', 'samesite'].indexOf(
key.toLowerCase()
) === -1
) {
continue;
}
this.pairs[key] = value;
}
}

/**
* Returns a raw string of the cookie.
*/
public rawString(): string {
return Object.keys(this.pairs)
.map((key) => {
if (key) {
return `${key}=${this.pairs[key]}`;
}
return this.pairs[key];
})
.join('; ');
}

/**
*
*/
public cookieString(): string {
if (this.key) {
return `${this.key}=${this.value}`;
}
return this.value;
}

/**
*
*/
public isExpired(): boolean {
// If the expries/maxage is set, then determine whether it is expired.
if (this.expriesOrMaxAge && this.expriesOrMaxAge.getTime() < Date.now()) {
return true;
}
// If the expries/maxage is not set, it's a session-level cookie that will expire when the browser is closed.
// (it's never expired in happy-dom)
return false;
}

/**
*
*/
public isHttpOnly(): boolean {
return this.httpOnly;
}

/**
*
*/
public isSecure(): boolean {
return this.secure;
}

/**
* Parse a cookie string.
*
* @param cookieString
*/
public static parse(cookieString: string): Cookie {
return new Cookie(cookieString);
}

/**
* Stringify a Cookie object.
*
* @param cookie
*/
public static stringify(cookie: Cookie): string {
return cookie.toString();
}
}
82 changes: 82 additions & 0 deletions packages/happy-dom/src/cookie/CookieJar.ts
@@ -0,0 +1,82 @@
import Location from 'src/location/Location';
import Cookie from './Cookie';

/**
* CookieJar.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie.
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie.
*/
export default class CookieJar {
private cookies: Cookie[] = [];

/**
* Validate cookie.
*
* @param cookie
*/
private validateCookie(cookie: Cookie): boolean {
if (cookie.key.toLowerCase().startsWith('__secure-') && !cookie.isSecure()) {
return false;
}
if (
cookie.key.toLowerCase().startsWith('__host-') &&
(!cookie.isSecure() || cookie.path !== '/' || cookie.domain)
) {
return false;
}
return true;
}

/**
* Set cookie.
*
* @param cookieString
*/
public setCookiesString(cookieString: string): void {
if (!cookieString) {
return;
}
const newCookie = new Cookie(cookieString);
if (!this.validateCookie(newCookie)) {
return;
}
this.cookies
.filter((cookie) => cookie.key === newCookie.key)
.forEach((cookie) => {
this.cookies.splice(this.cookies.indexOf(cookie), 1);
});
this.cookies.push(newCookie);
}

/**
* Get cookie.
*
* @param location Location.
* @param fromDocument If true, the caller is a document.
* @returns Cookie string.
*/
public getCookiesString(location: Location, fromDocument: boolean): string {
const cookies = this.cookies.filter((cookie) => {
// Skip when use document.cookie and the cookie is httponly.
if (fromDocument && cookie.isHttpOnly()) {
return false;
}
if (cookie.isExpired()) {
return false;
}
if (cookie.isSecure() && location.protocol !== 'https:') {
return false;
}
if (cookie.domain && !location.hostname.endsWith(cookie.domain)) {
return false;
}
if (cookie.path && !location.pathname.startsWith(cookie.path)) {
return false;
}
// TODO: check SameSite.
return true;
});
return cookies.map((cookie) => cookie.cookieString()).join('; ');
}
}
87 changes: 0 additions & 87 deletions packages/happy-dom/src/cookie/CookieUtility.ts

This file was deleted.

10 changes: 6 additions & 4 deletions packages/happy-dom/src/nodes/document/Document.ts
Expand Up @@ -19,7 +19,7 @@ import QuerySelector from '../../query-selector/QuerySelector';
import IDocument from './IDocument';
import CSSStyleSheet from '../../css/CSSStyleSheet';
import DOMException from '../../exception/DOMException';
import CookieUtility from '../../cookie/CookieUtility';
import CookieJar from '../../cookie/CookieJar';
import IElement from '../element/IElement';
import IHTMLScriptElement from '../html-script-element/IHTMLScriptElement';
import IHTMLElement from '../html-element/IHTMLElement';
Expand Down Expand Up @@ -63,10 +63,12 @@ export default class Document extends Node implements IDocument {

// Used as an unique identifier which is updated whenever the DOM gets modified.
public _cacheID = 0;
// Public in order to be accessible by the fetch and xhr.
public _cookie = new CookieJar();

protected _isFirstWrite = true;
protected _isFirstWriteAfterOpen = false;
private _cookie = '';

private _selection: Selection = null;

// Events
Expand Down Expand Up @@ -258,7 +260,7 @@ export default class Document extends Node implements IDocument {
* @returns Cookie.
*/
public get cookie(): string {
return this._cookie;
return this._cookie.getCookiesString(this.defaultView.location, true);
}

/**
Expand All @@ -267,7 +269,7 @@ export default class Document extends Node implements IDocument {
* @param cookie Cookie string.
*/
public set cookie(cookie: string) {
this._cookie = CookieUtility.getCookieString(this.defaultView.location, this._cookie, cookie);
this._cookie.setCookiesString(cookie);
}

/**
Expand Down

0 comments on commit b9ecfc7

Please sign in to comment.