merge latest node-fetch-cookies version
This commit is contained in:
parent
bd2009cf72
commit
0c767b3fee
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "node-fetch-cookies",
|
||||
"version": "1.0.6",
|
||||
"name": "flumm-fetch-cookies",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "flumm-fetch-cookies",
|
||||
"version": "1.0.6",
|
||||
"version": "1.1.0",
|
||||
"description": "flumm-fetch wrapper that adds support for cookie-jars",
|
||||
"main": "src/index.mjs",
|
||||
"engines": {
|
||||
|
|
|
@ -1,15 +1,69 @@
|
|||
import Cookie from "./cookie";
|
||||
import url from "url";
|
||||
|
||||
export default class CookieJar {
|
||||
constructor() {
|
||||
this.cookies = new Map();
|
||||
}
|
||||
addCookie(c, fromURL) {
|
||||
if(typeof c === "string")
|
||||
if(typeof c === "string") {
|
||||
try {
|
||||
c = new Cookie(c, fromURL);
|
||||
this.cookies.set(c.name, c);
|
||||
}
|
||||
forEach(callback) {
|
||||
this.cookies.forEach(callback);
|
||||
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))
|
||||
throw new TypeError("First parameter is neither a string nor a cookie!");
|
||||
if(!this.cookies.get(c.domain))
|
||||
this.cookies.set(c.domain, new Map());
|
||||
this.cookies.get(c.domain).set(c.name, c);
|
||||
return true;
|
||||
}
|
||||
domains() {
|
||||
return this.cookies.keys();
|
||||
}
|
||||
*cookiesDomain(domain) {
|
||||
for(const cookie of (this.cookies.get(domain) || []).values())
|
||||
yield cookie;
|
||||
}
|
||||
*cookiesValid(withSession) {
|
||||
for(const cookie of this.cookiesAll())
|
||||
if(!cookie.hasExpired(!withSession))
|
||||
yield cookie;
|
||||
}
|
||||
*cookiesAll() {
|
||||
for(const domain of this.domains())
|
||||
yield* this.cookiesDomain(domain);
|
||||
}
|
||||
*cookiesValidForRequest(requestURL) {
|
||||
const namesYielded = [],
|
||||
domains = url
|
||||
.parse(requestURL)
|
||||
.hostname
|
||||
.split(".")
|
||||
.map((_, i, a) => a.slice(i).join("."))
|
||||
.slice(0, -1);
|
||||
for(const domain of domains) {
|
||||
for(const cookie of this.cookiesDomain(domain)) {
|
||||
if(cookie.isValidForRequest(requestURL)
|
||||
&& namesYielded.every(name => name !== cookie.name)) {
|
||||
namesYielded.push(cookie.name);
|
||||
yield cookie;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
deleteExpired(sessionEnded) {
|
||||
const validCookies = [...this.cookiesValid(!sessionEnded)];
|
||||
this.cookies = new Map();
|
||||
validCookies.forEach(c => this.addCookie(c));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import urlParser from "url";
|
||||
import url from "url";
|
||||
|
||||
class CookieParseError extends Error {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.name = "CookieParseError";
|
||||
}
|
||||
}
|
||||
|
||||
const validateHostname = (cookieHostname, requestHostname, subdomains) => {
|
||||
cookieHostname = cookieHostname.toLowerCase();
|
||||
|
@ -9,8 +16,8 @@ const validateHostname = (cookieHostname, requestHostname, subdomains) => {
|
|||
};
|
||||
|
||||
const validatePath = (cookiePath, requestPath) => {
|
||||
cookiePath = cookiePath.toLowerCase();
|
||||
requestPath = requestPath.toLowerCase();
|
||||
cookiePath = decodeURIComponent(cookiePath).toLowerCase();
|
||||
requestPath = decodeURIComponent(requestPath).toLowerCase();
|
||||
if(cookiePath.endsWith("/"))
|
||||
cookiePath = cookiePath.slice(0, -1);
|
||||
if(requestPath.endsWith("/"))
|
||||
|
@ -18,18 +25,31 @@ const validatePath = (cookiePath, requestPath) => {
|
|||
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, url) {
|
||||
constructor(str, requestURL) {
|
||||
if(typeof str !== "string")
|
||||
throw new TypeError("Input not a string");
|
||||
throw new TypeError("First parameter is not a string!");
|
||||
|
||||
const splitted = str.split("; ");
|
||||
[this.name, this.value] = splitted[0].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] = splitted[i].split("=");
|
||||
let [k, v] = splitN(splitted[i], "=", 1);
|
||||
k = k.toLowerCase();
|
||||
if(v) {
|
||||
if(k === "expires") {
|
||||
|
@ -37,28 +57,29 @@ export default class Cookie {
|
|||
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 TypeError("Invalid value for Expires \"" + v + "\"!");
|
||||
throw new CookieParseError("Invalid value for Expires \"" + v + "\"!");
|
||||
}
|
||||
else if(k === "max-age") {
|
||||
const seconds = parseInt(v);
|
||||
const seconds = ~~+v;
|
||||
if(seconds.toString() !== v)
|
||||
throw new TypeError("Invalid value for Max-Age \"" + 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") {
|
||||
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 TypeError("Invalid key \"" + k + "\" specified!");
|
||||
throw new CookieParseError("Invalid key \"" + k + "\" with value \"" + v + "\" specified!");
|
||||
}
|
||||
else {
|
||||
if(k === "secure")
|
||||
|
@ -66,43 +87,41 @@ export default class Cookie {
|
|||
else if(k === "httponly") // only relevant for browsers
|
||||
continue;
|
||||
else
|
||||
throw new TypeError("Invalid key \"" + k + "\" specified!");
|
||||
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 = urlParser.parse(url).hostname;
|
||||
this.domain = parsedURL.hostname;
|
||||
this.subdomains = false;
|
||||
}
|
||||
if(!this.path)
|
||||
this.path = "/";
|
||||
if(this.name.toLowerCase().startsWith("__secure-") && (!this.secure || !url.toLowerCase().startsWith("https:")))
|
||||
throw new TypeError("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 || !url.toLowerCase().startsWith("https:") || this.domain || this.path !== "/"))
|
||||
throw new TypeError("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 \"/\"!");
|
||||
}
|
||||
static fromObject(obj) {
|
||||
let c = Object.assign(Object.create(this.prototype), obj);
|
||||
if(c.expiry && typeof c.expiry === "string")
|
||||
c.expiry = new Date(c.expiry);
|
||||
return c;
|
||||
if(!this.secure)
|
||||
this.secure = false;
|
||||
if(!this.expiry)
|
||||
this.expiry = null;
|
||||
}
|
||||
serialize() {
|
||||
return this.name + "=" + this.value;
|
||||
}
|
||||
hasExpired() {
|
||||
return this.expiry && this.expiry < new Date();
|
||||
hasExpired(sessionEnded) {
|
||||
return sessionEnded && this.expiry === null || this.expiry < new Date();
|
||||
}
|
||||
isValidForRequest(url) {
|
||||
if(this.hasExpired())
|
||||
isValidForRequest(requestURL) {
|
||||
if(this.hasExpired(false))
|
||||
return false;
|
||||
const parsedURL = urlParser.parse(url);
|
||||
if(parsedURL.protocol !== "http:" && parsedURL.protocol !== "https:")
|
||||
return false;
|
||||
if(this.secure && parsedURL.protocol !== "https:")
|
||||
return false;
|
||||
if(!validateHostname(this.domain, parsedURL.hostname, this.subdomains))
|
||||
return false;
|
||||
if(!validatePath(this.path, parsedURL.pathname))
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
import fetch from "./fetch";
|
||||
import CookieJar from "./cookie-jar";
|
||||
import Cookie from "./cookie";
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
export default async function cookieFetch(url, options) {
|
||||
let cookies = "";
|
||||
cookieJar.forEach(c => {
|
||||
if(c.isValidForRequest(url))
|
||||
cookies += c.serialize() + "; ";
|
||||
});
|
||||
if(cookies.length !== 0) {
|
||||
if(!options) {
|
||||
options = {
|
||||
headers: {}
|
||||
};
|
||||
}
|
||||
else if(!options.headers)
|
||||
[...cookieJar.cookiesValidForRequest(url)]
|
||||
.filter((v, i, a) => a.slice(0, i).every(c => c.name !== v.name)) // filter cookies with duplicate names
|
||||
.forEach(c => cookies += c.serialize() + "; ");
|
||||
|
||||
if(cookies) {
|
||||
if(!options)
|
||||
options = {};
|
||||
if(!options.headers)
|
||||
options.headers = {};
|
||||
options.headers.cookie = cookies.slice(0, -2);
|
||||
}
|
||||
|
||||
const result = await fetch(url, options);
|
||||
cookies = result.headers["set-cookie"];
|
||||
if(cookies)
|
||||
|
||||
cookies = result.headers["set-cookie"] || [];
|
||||
cookies.forEach(c => cookieJar.addCookie(c, url));
|
||||
|
||||
// delete expired cookies after each request
|
||||
cookieJar.deleteExpired(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user