From d92f8d35646f82ba9b3dfe7f13be1486fd4b4c25 Mon Sep 17 00:00:00 2001 From: jkhsjdhjs Date: Sun, 13 Jan 2019 23:30:17 +0100 Subject: [PATCH] initial commit --- .gitignore | 1 + LICENSE | 21 ++++++++++ package-lock.json | 13 ++++++ package.json | 31 ++++++++++++++ src/cookie-jar.mjs | 43 +++++++++++++++++++ src/cookie.mjs | 100 +++++++++++++++++++++++++++++++++++++++++++++ src/index.mjs | 40 ++++++++++++++++++ src/test.mjs | 5 +++ 8 files changed, 254 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/cookie-jar.mjs create mode 100644 src/cookie.mjs create mode 100644 src/index.mjs create mode 100644 src/test.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2612608 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 jkhsjdhjs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9fb95c3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "node-fetch-cookies", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "node-fetch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", + "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..27ff2bd --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "node-fetch-cookies", + "version": "1.0.0", + "description": "node-fetch wrapper that adds support for cookie-jars", + "main": "src/index.mjs", + "engines": { + "node": ">=10.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jkhsjdhjs/node-fetch-cookies.git" + }, + "keywords": [ + "cookie", + "cookie-jar", + "node-fetch", + "fetch" + ], + "author": "jkhsjdhjs", + "license": "MIT", + "bugs": { + "url": "https://github.com/jkhsjdhjs/node-fetch-cookies/issues" + }, + "homepage": "https://github.com/jkhsjdhjs/node-fetch-cookies#readme", + "dependencies": { + "node-fetch": "^2.3.0" + } +} diff --git a/src/cookie-jar.mjs b/src/cookie-jar.mjs new file mode 100644 index 0000000..6da8f52 --- /dev/null +++ b/src/cookie-jar.mjs @@ -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])); + } +}; diff --git a/src/cookie.mjs b/src/cookie.mjs new file mode 100644 index 0000000..81dbff0 --- /dev/null +++ b/src/cookie.mjs @@ -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; + } +}; diff --git a/src/index.mjs b/src/index.mjs new file mode 100644 index 0000000..8db2310 --- /dev/null +++ b/src/index.mjs @@ -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 +}; diff --git a/src/test.mjs b/src/test.mjs new file mode 100644 index 0000000..b1ab266 --- /dev/null +++ b/src/test.mjs @@ -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();