jkhsjdhjs 5c6ccbd3e8
improve performance by using a Set instead of array filtering in fetch and CookieJar.cookiesValidForRequest()
use fsPromises API instead of the synchronous one
move cookie loading to seperate async CookieJar.load() function
make cookie saving async (CookieJar.save())
add helper function for creating type errors to error.mjs
move CookieParseError to error.mjs
change readme according to changes
bump minor version to 1.3.0
this version requires at least nodejs 11.14.0
2019-11-25 06:30:12 +01:00

129 lines
5.4 KiB
JavaScript

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");
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)) === "Invalid Date")
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 && 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;
}
};