initial commit

This commit is contained in:
2019-01-13 23:30:17 +01:00
commit d92f8d3564
8 changed files with 254 additions and 0 deletions

43
src/cookie-jar.mjs Normal file
View File

@ -0,0 +1,43 @@
import fs from "fs";
import Cookie from "./cookie";
export default class CookieJar {
constructor(flags, file, cookies) {
this.cookies = new Map();
this.file = file;
this.flags = flags;
if(typeof this.flags !== "string")
throw new TypeError("First parameter is not a string!");
if(typeof this.file !== "string")
throw new TypeError("Second parameter is not a string!");
if(Array.isArray(cookies)) {
if(!cookies.every(c => c instanceof Cookie))
throw new TypeError("Third parameter is not an array of cookies!");
else
cookies.forEach(cookie => this.cookies.set(cookie.name, cookie));
}
else if(cookies instanceof Cookie)
this.cookies.set(cookies.name, cookies);
else
throw new TypeError("Third parameter is neither an array nor a cookie!");
if(this.cookies.size === 0 && this.file.length !== 0 && fs.existsSync(this.file))
this.cookies = new Map(JSON.parse(fs.readFileSync(this.file)));
}
addCookie(c) {
if(typeof c === "string")
c = new Cookie(c);
this.cookies.set(c.name, c);
}
forEach(callback) {
this.cookies.forEach(callback);
}
save() {
// only save cookies that haven't expired
let cookiesToSave = new Map();
this.forEach(cookie => {
if(cookie.expiry && cookie.expiry > new Date())
cookiesToSave.set(cookie.name, cookie);
});
fs.writeFileSync(this.file, JSON.stringify([...cookiesToSave]));
}
};

100
src/cookie.mjs Normal file
View File

@ -0,0 +1,100 @@
import urlParser from "url";
const validateHostname = (cookieHostname, requestHostname, subdomains) => {
if(requestHostname === cookieHostname || (subdomains && requestHostname.endsWith("." + cookieHostname)))
return true;
return false;
};
const validatePath = (cookiePath, requestPath) => {
if(cookiePath.endsWith("/"))
cookiePath = cookiePath.slice(0, -1);
if(requestPath.endsWith("/"))
requestPath = requestPath.slice(0, -1);
return (requestPath + "/").startsWith(cookiePath + "/");
};
export default class Cookie {
constructor(str, url) {
if(typeof str !== "string")
throw new TypeError("Input not a string");
const splitted = str.split("; ");
[this.name, this.value] = splitted[0].split("=");
if(this.value.startsWith("\"") && this.value.endsWith("\""))
this.value = this.value.slice(1, -1);
this.value = this.value;
for(let i = 1; i < splitted.length; i++) {
let [k, v] = splitted[i].split("=");
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{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 + "\"!");
}
else if(k === "max-age") {
const seconds = parseInt(v);
if(seconds.toString() !== v)
throw new TypeError("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);
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 TypeError("Invalid key \"" + k + "\" specified!");
}
else {
if(k === "secure")
this.secure = true;
else if(k === "httponly") // only relevant for browsers
continue;
else
throw new TypeError("Invalid key \"" + k + "\" specified!");
}
}
if(!this.domain) {
this.domain = urlParser.parse(url).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) {
return Object.assign(Object.create(this.prototype), obj);
}
serialize() {
return this.name + "=" + this.value;
}
isValidForRequest(url) {
if(this.expiry && this.expiry > new Date())
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))
return false;
return true;
}
};

40
src/index.mjs Normal file
View File

@ -0,0 +1,40 @@
import fetch from "node-fetch";
import CookieJar from "./cookie-jar";
import Cookie from "./cookie";
export default {
fetch: async (url, options, cookieJars) => {
let cookies = "";
if(Array.isArray(cookieJars) && cookieJars.every(c => c instanceof CookieJar)) {
cookieJars.forEach(jar => {
if(!jar.flags.includes("r"))
return;
jar.forEach(c => c.isValidForRequest(url) && (cookies += c.serialize() + "; "));
});
}
else if(cookieJars instanceof CookieJar && cookieJars.flags.includes("r")) {
cookieJars.forEach(c => c.isValidForRequest(url) && (cookies += c.serialize() + "; "));
}
else
throw new TypeError("Third paramter is neither a cookie jar nor an array of cookie jars!");
if(cookies.length !== 0)
options.headers["Cookie"] = cookies.slice(0, -2);
const result = await fetch(url, options);
// i cannot use headers.get() here because it joins the cookies to a string
cookies = result.headers[Object.getOwnPropertySymbols(result.headers)[0]]["set-cookie"];
if(cookies) {
if(Array.isArray(cookieJars)) {
cookieJars.forEach(jar => {
if(!jar.flags.includes("w"))
return;
cookies.forEach(c => jar.addCookie(c));
});
}
else if(cookieJars.flags.includes("w")) {
cookies.forEach(c => cookieJars.addCookie(c));
}
}
},
CookieJar: CookieJar,
Cookie: Cookie
};

5
src/test.mjs Normal file
View File

@ -0,0 +1,5 @@
import cookieFetch from "./index.mjs";
import fetch from "node-fetch";
//console.log(new cookieFetch());
fetch("https://google.de").then(x => console.log();