Skip to content

Commit

Permalink
capricorn86#666@patch: Fixes Cookie Manager.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mas0nShi committed Nov 30, 2022
1 parent 414e6ab commit 56fa772
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 91 deletions.
122 changes: 122 additions & 0 deletions packages/happy-dom/src/cookie/Cookie.ts
@@ -0,0 +1,122 @@
const CookiePairRegex = /([^=]+)(?:=([\s\S]*))?/;


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


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;
}
}

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;
}

public static parse(cookieString: string): Cookie {
return new Cookie(cookieString);
}

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


export default class CookieJar {
private cookies: Cookie[] = [];


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

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);
}

// return the cookie string.
// skip httponly when use document.cookie.
public getCookiesString(url: 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() && url.protocol !== "https:") return false;
if (cookie.domain && !url.hostname.endsWith(cookie.domain)) return false;
if (cookie.path && !url.pathname.startsWith(cookie.path)) return false;
// TODO: check SameSite.
/*
switch (cookie.getSameSite()) {
case "Strict":
// means that the browser sends the cookie only for same-site requests, that is, requests originating from the same site that set the cookie. If a request originates from a different domain or scheme (even with the same domain), no cookies with the SameSite=Strict attribute are sent.
if (url.hostname !== cookie.getDomain()) return false;
break;
case "Lax":
// means that the cookie is not sent on cross-site requests, such as on requests to load images or frames, but is sent when a user is navigating to the origin site from an external site (for example, when following a link). This is the default behavior if the SameSite attribute is not specified.
if (url.hostname !== cookie.getDomain()) return false;
break;
case "None":
// means that the browser sends the cookie for same-site requests, and for cross-site requests with a CORS header. This is the default value if the SameSite attribute is not specified.
break;
default:
break;
}
*/
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.

8 changes: 4 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 @@ -66,7 +66,7 @@ export default class Document extends Node implements IDocument {

protected _isFirstWrite = true;
protected _isFirstWriteAfterOpen = false;
private _cookie = '';
private _cookie = new CookieJar();
private _selection: Selection = null;

// Events
Expand Down Expand Up @@ -258,7 +258,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 +267,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 56fa772

Please sign in to comment.