import url from "url"; import {paramError, CookieParseError} from "./errors.mjs"; const validateHostname = (cookieHostname, requestHostname, subdomains) => { cookieHostname = cookieHostname.toLowerCase(); requestHostname = requestHostname.toLowerCase(); if(requestHostname === cookieHostname || (subdomains && requestHostname.endsWith("." + cookieHostname))) return true; return false; }; const validatePath = (cookiePath, requestPath) => { cookiePath = decodeURIComponent(cookiePath).toLowerCase(); requestPath = decodeURIComponent(requestPath).toLowerCase(); if(cookiePath.endsWith("/")) cookiePath = cookiePath.slice(0, -1); if(requestPath.endsWith("/")) requestPath = requestPath.slice(0, -1); return (requestPath + "/").startsWith(cookiePath + "/"); }; const splitN = (str, sep, n) => { const splitted = str.split(sep); if(n < splitted.length - 1) { splitted[n] = splitted.slice(n).join(sep); splitted.splice(n + 1); } return splitted; }; export default class Cookie { constructor(str, requestURL) { if(typeof str !== "string") throw paramError("First", "str", "new Cookie()", "string"); if(typeof requestURL !== "string") throw paramError("Second", "requestURL", "new Cookie()", "string"); // check if url is valid new url.URL(requestURL); const splitted = str.split("; "); [this.name, this.value] = splitN(splitted[0], "=", 1); if(!this.name) throw new CookieParseError("Invalid cookie name \"" + this.name + "\"!"); if(this.value.startsWith("\"") && this.value.endsWith("\"")) this.value = this.value.slice(1, -1); const parsedURL = url.parse(requestURL); for(let i = 1; i < splitted.length; i++) { let [k, v] = splitN(splitted[i], "=", 1); k = k.toLowerCase(); if(v) { if(k === "expires") { if(this.expiry) // max-age has precedence over expires continue; if(!/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2}[ -](?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[ -]\d{2,4} \d{2}:\d{2}:\d{2} GMT$/.test(v) || (this.expiry = new Date(v)).toString() === "Invalid Date" || this.expiry.getTime() < 0) throw new CookieParseError("Invalid value for Expires \"" + v + "\"!"); } else if(k === "max-age") { const seconds = ~~+v; if(seconds.toString() !== v) throw new CookieParseError("Invalid value for Max-Age \"" + v + "\"!"); this.expiry = new Date(); this.expiry.setSeconds(this.expiry.getSeconds() + seconds); } else if(k === "domain") { if(v.startsWith(".")) v = v.substring(1); if(!validateHostname(parsedURL.hostname, v, true)) throw new CookieParseError("Invalid value for Domain \"" + v + "\": cookie was received from \"" + parsedURL.hostname + "\"!"); this.domain = v; this.subdomains = true; } else if(k === "path") this.path = v; else if(k === "samesite") // only relevant for cross site requests, so not for us continue; else throw new CookieParseError("Invalid key \"" + k + "\" with value \"" + v + "\" specified!"); } else { if(k === "secure") this.secure = true; else if(k === "httponly") // only relevant for browsers continue; else throw new CookieParseError("Invalid key \"" + k + "\" specified!"); } } if(this.name.toLowerCase().startsWith("__secure-") && (!this.secure || parsedURL.protocol !== "https:")) throw new CookieParseError("Cookie has \"__Secure-\" prefix but \"Secure\" isn't set or the cookie is not set via https!"); if(this.name.toLowerCase().startsWith("__host-") && (!this.secure || parsedURL.protocol !== "https:" || this.domain || this.path !== "/")) throw new CookieParseError("Cookie has \"__Host-\" prefix but \"Secure\" isn't set, the cookie is not set via https, \"Domain\" is set or \"Path\" is not equal to \"/\"!"); // assign defaults if(!this.domain) { this.domain = parsedURL.hostname; this.subdomains = false; } if(!this.path) this.path = "/"; if(!this.secure) this.secure = false; if(!this.expiry) this.expiry = null; } static fromObject(obj) { let c = Object.assign(Object.create(this.prototype), obj); if(typeof c.expiry === "string") c.expiry = new Date(c.expiry); return c; } serialize() { return this.name + "=" + this.value; } hasExpired(sessionEnded) { return sessionEnded && this.expiry === null || this.expiry && this.expiry < new Date(); } isValidForRequest(requestURL) { if(this.hasExpired(false)) return false; const parsedURL = url.parse(requestURL); if(parsedURL.protocol !== "http:" && parsedURL.protocol !== "https:" || this.secure && parsedURL.protocol !== "https:" || !validateHostname(this.domain, parsedURL.hostname, this.subdomains) || !validatePath(this.path, parsedURL.pathname)) return false; return true; } };