add CookieParseError class

catch CookieParseErrors when adding to jar, log a warning
add option to delete session cookies to CookieJar.deleteExpired()
cookie path may be uri encoded. decode it when comparing
check cookie domain attribute validity when parsing
minor version bump
This commit is contained in:
jkhsjdhjs 2019-08-15 13:32:49 +02:00
parent 7e7a19403c
commit f902fa6379
Signed by: jkhsjdhjs
GPG Key ID: BAC6ADBAB7D576CC
6 changed files with 49 additions and 22 deletions

View File

@ -46,6 +46,9 @@ Adds a cookie to the jar.
- `cookie` A [Cookie](#class-cookie) instance to add to the cookie jar. Alternatively this can also be a string, for example a serialized cookie received from a website. In this case `url` should be specified. - `cookie` A [Cookie](#class-cookie) instance to add to the cookie jar. Alternatively this can also be a string, for example a serialized cookie received from a website. In this case `url` should be specified.
- `url` The url a cookie has been received from. - `url` The url a cookie has been received from.
Returns `true` if the cookie has been added successfully. Returns `false` otherwise.
Will log a warning to console if a cookie fails to be parsed.
#### addFromFile(file) #### addFromFile(file)
Reads a cookie jar from the disk and adds the contained cookies. Reads a cookie jar from the disk and adds the contained cookies.
@ -64,8 +67,8 @@ Returns an iterator over all cookies currently stored.
#### *cookiesValidForRequest(url) #### *cookiesValidForRequest(url)
Returns an iterator over all cookies valid for a request to `url`. Returns an iterator over all cookies valid for a request to `url`.
#### deleteExpired() #### deleteExpired(sessionEnded)
Removes all expired cookies from the jar (including session cookies). Removes all expired cookies from the jar (including session cookies, if `sessionEnded` is `true`).
#### save() #### save()
Saves the cookie jar to disk. Only non-expired non-session cookies are saved. Saves the cookie jar to disk. Only non-expired non-session cookies are saved.
@ -87,6 +90,8 @@ An abstract representation of a cookie.
- `cookie` The string representation of a cookie as send by a webserver. - `cookie` The string representation of a cookie as send by a webserver.
- `url` The url the cookie has been received from. - `url` The url the cookie has been received from.
Will throw a `CookieParseError` if the input couldn't be parsed.
#### static fromObject(obj) #### static fromObject(obj)
Creates a cookie instance from an already existing object with the same properties. Creates a cookie instance from an already existing object with the same properties.
@ -99,5 +104,6 @@ Returns whether the cookie has expired or not. `sessionEnded` specifies whether
#### isValidForRequest(url) #### isValidForRequest(url)
Returns whether the cookie is valid for a request to `url`. If not, it won't be send by the fetch wrapper. Returns whether the cookie is valid for a request to `url`. If not, it won't be send by the fetch wrapper.
## License ## License
This project is licensed under the MIT license, see [LICENSE](LICENSE). This project is licensed under the MIT license, see [LICENSE](LICENSE).

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "node-fetch-cookies", "name": "node-fetch-cookies",
"version": "1.1.1", "version": "1.2.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "node-fetch-cookies", "name": "node-fetch-cookies",
"version": "1.1.1", "version": "1.2.0",
"description": "node-fetch wrapper that adds support for cookie-jars", "description": "node-fetch wrapper that adds support for cookie-jars",
"main": "src/index.mjs", "main": "src/index.mjs",
"engines": { "engines": {

View File

@ -27,13 +27,26 @@ export default class CookieJar {
throw new TypeError("Third parameter is neither an array nor a cookie!"); throw new TypeError("Third parameter is neither an array nor a cookie!");
} }
addCookie(c, fromURL) { addCookie(c, fromURL) {
if(typeof c === "string") if(typeof c === "string") {
try {
c = new Cookie(c, fromURL); c = new Cookie(c, fromURL);
}
catch(error) {
if(error.name === "CookieParseError") {
console.warn("Ignored cookie: " + c);
console.warn("Reason: " + error.message);
return false;
}
else
throw error;
}
}
else if(!(c instanceof Cookie)) else if(!(c instanceof Cookie))
throw new TypeError("First parameter is neither a string nor a cookie!"); throw new TypeError("First parameter is neither a string nor a cookie!");
if(!this.cookies.get(c.domain)) if(!this.cookies.get(c.domain))
this.cookies.set(c.domain, new Map()); this.cookies.set(c.domain, new Map());
this.cookies.get(c.domain).set(c.name, c); this.cookies.get(c.domain).set(c.name, c);
return true;
} }
addFromFile(file) { addFromFile(file) {
JSON.parse(fs.readFileSync(this.file)).forEach(c => this.addCookie(Cookie.fromObject(c))); JSON.parse(fs.readFileSync(this.file)).forEach(c => this.addCookie(Cookie.fromObject(c)));
@ -72,8 +85,8 @@ export default class CookieJar {
} }
} }
} }
deleteExpired() { deleteExpired(sessionEnded) {
const validCookies = [...this.cookiesValid(false)]; const validCookies = [...this.cookiesValid(!sessionEnded)];
this.cookies = new Map(); this.cookies = new Map();
validCookies.forEach(c => this.addCookie(c)); validCookies.forEach(c => this.addCookie(c));
} }

View File

@ -1,5 +1,12 @@
import url from "url"; import url from "url";
class CookieParseError extends Error {
constructor(...args) {
super(...args);
this.name = "CookieParseError";
}
}
const validateHostname = (cookieHostname, requestHostname, subdomains) => { const validateHostname = (cookieHostname, requestHostname, subdomains) => {
cookieHostname = cookieHostname.toLowerCase(); cookieHostname = cookieHostname.toLowerCase();
requestHostname = requestHostname.toLowerCase(); requestHostname = requestHostname.toLowerCase();
@ -9,8 +16,8 @@ const validateHostname = (cookieHostname, requestHostname, subdomains) => {
}; };
const validatePath = (cookiePath, requestPath) => { const validatePath = (cookiePath, requestPath) => {
cookiePath = cookiePath.toLowerCase(); cookiePath = decodeURIComponent(cookiePath).toLowerCase();
requestPath = requestPath.toLowerCase(); requestPath = decodeURIComponent(requestPath).toLowerCase();
if(cookiePath.endsWith("/")) if(cookiePath.endsWith("/"))
cookiePath = cookiePath.slice(0, -1); cookiePath = cookiePath.slice(0, -1);
if(requestPath.endsWith("/")) if(requestPath.endsWith("/"))
@ -35,10 +42,12 @@ export default class Cookie {
const splitted = str.split("; "); const splitted = str.split("; ");
[this.name, this.value] = splitN(splitted[0], "=", 1); [this.name, this.value] = splitN(splitted[0], "=", 1);
if(!this.name) if(!this.name)
throw new Error("Invalid cookie name \"" + this.name + "\""); throw new CookieParseError("Invalid cookie name \"" + this.name + "\"");
if(this.value.startsWith("\"") && this.value.endsWith("\"")) if(this.value.startsWith("\"") && this.value.endsWith("\""))
this.value = this.value.slice(1, -1); this.value = this.value.slice(1, -1);
const parsedURL = url.parse(requestURL);
for(let i = 1; i < splitted.length; i++) { for(let i = 1; i < splitted.length; i++) {
let [k, v] = splitN(splitted[i], "=", 1); let [k, v] = splitN(splitted[i], "=", 1);
k = k.toLowerCase(); k = k.toLowerCase();
@ -48,18 +57,20 @@ export default class Cookie {
continue; 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) 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") || (this.expiry = new Date(v)) === "Invalid Date")
throw new Error("Invalid value for Expires \"" + v + "\"!"); throw new CookieParseError("Invalid value for Expires \"" + v + "\"!");
} }
else if(k === "max-age") { else if(k === "max-age") {
const seconds = ~~+v; const seconds = ~~+v;
if(seconds.toString() !== v) if(seconds.toString() !== v)
throw new Error("Invalid value for Max-Age \"" + v + "\"!"); throw new CookieParseError("Invalid value for Max-Age \"" + v + "\"!");
this.expiry = new Date(); this.expiry = new Date();
this.expiry.setSeconds(this.expiry.getSeconds() + seconds); this.expiry.setSeconds(this.expiry.getSeconds() + seconds);
} }
else if(k === "domain") { else if(k === "domain") {
if(v.startsWith(".")) if(v.startsWith("."))
v = v.substring(1); 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.domain = v;
this.subdomains = true; this.subdomains = true;
} }
@ -69,7 +80,7 @@ export default class Cookie {
else if(k === "samesite") // only relevant for cross site requests, so not for us else if(k === "samesite") // only relevant for cross site requests, so not for us
continue; continue;
else else
throw new Error("Invalid key \"" + k + "\" with value \"" + v + "\" specified!"); throw new CookieParseError("Invalid key \"" + k + "\" with value \"" + v + "\" specified!");
} }
else { else {
if(k === "secure") if(k === "secure")
@ -77,16 +88,14 @@ export default class Cookie {
else if(k === "httponly") // only relevant for browsers else if(k === "httponly") // only relevant for browsers
continue; continue;
else else
throw new Error("Invalid key \"" + k + "\" without value specified!"); throw new CookieParseError("Invalid key \"" + k + "\" specified!");
} }
} }
const parsedURL = url.parse(requestURL);
if(this.name.toLowerCase().startsWith("__secure-") && (!this.secure || parsedURL.protocol !== "https:")) if(this.name.toLowerCase().startsWith("__secure-") && (!this.secure || parsedURL.protocol !== "https:"))
throw new Error("Cookie has \"__Secure-\" prefix but \"Secure\" isn't set or the cookie is not set via 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)) if(this.name.toLowerCase().startsWith("__host-") && (!this.secure || parsedURL.protocol !== "https:" || this.domain || (this.path && this.path !== "/")))
throw new Error("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 \"/\"!"); 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 // assign defaults
if(!this.domain) { if(!this.domain) {

View File

@ -34,10 +34,9 @@ async function cookieFetch(cookieJars, url, options) {
.filter(jar => jar.flags.includes("w")) .filter(jar => jar.flags.includes("w"))
.forEach(jar => cookies.forEach(c => jar.addCookie(c, url))); .forEach(jar => cookies.forEach(c => jar.addCookie(c, url)));
} }
else if(cookieJars instanceof CookieJar && cookieJars.flags.includes("w")) { else if(cookieJars instanceof CookieJar && cookieJars.flags.includes("w"))
cookies.forEach(c => cookieJars.addCookie(c, url)); cookies.forEach(c => cookieJars.addCookie(c, url));
} }
}
return result; return result;
} }