flumm-fetch-cookies
Version:
flumm-fetch wrapper that adds support for cookie-jars
175 lines (164 loc) • 6.66 kB
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");
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;
}
}