forked from capricorn86/happy-dom
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
capricorn86#666@patch: Fixes Cookie Manager.
- Loading branch information
Showing
4 changed files
with
187 additions
and
91 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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("; "); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters