merge latest node-fetch-cookies version
This commit is contained in:
commit
6bde778519
7
.travis.yml
Normal file
7
.travis.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- '13'
|
||||||
|
install:
|
||||||
|
- npm install
|
||||||
|
script:
|
||||||
|
- npm test
|
125
README.md
125
README.md
|
@ -1,29 +1,59 @@
|
||||||
# flumm-fetch-cookies
|
# flumm-fetch-cookies [![Build Status](https://travis-ci.org/kein-Bot/flumm-fetch-cookies.svg?branch=master)](https://travis-ci.org/kein-Bot/flumm-fetch-cookies)
|
||||||
A [flumm-fetch](https://gitfap.de/Flummi/kbotv3-modules/blob/master/src/inc/fetch.mjs) wrapper with support for cookies.
|
A [flumm-fetch](https://github.com/kein-Bot/flumm-fetch) wrapper with support for cookies.
|
||||||
It supports reading/writing from/to a JSON cookie jar and keeps cookies in memory until you call `CookieJar.save()` to reduce disk I/O.
|
It supports reading/writing from/to a JSON cookie jar and keeps cookies in memory until you call `CookieJar.save()` to reduce disk I/O.
|
||||||
|
|
||||||
## Usage Example
|
### For upgrading from 1.2.x or below to 1.3.x or above, please read the [breaking API changes](#130-breaking-api-changes).
|
||||||
|
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
### with file...
|
||||||
```javascript
|
```javascript
|
||||||
import {fetch, CookieJar} from "flumm-fetch-cookies";
|
import {fetch, CookieJar} from "flumm-fetch-cookies";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// creates a CookieJar instance
|
// creates a CookieJar instance
|
||||||
let cookieJar = new CookieJar("rw", "jar.json");
|
const cookieJar = new CookieJar("jar.json");
|
||||||
|
|
||||||
|
// load cookies from the cookie jar
|
||||||
|
await cookieJar.load();
|
||||||
|
|
||||||
// usual fetch usage, except with one or multiple cookie jars as first parameter
|
// usual fetch usage, except with one or multiple cookie jars as first parameter
|
||||||
const response = await fetch(cookieJar, "https://google.de");
|
const response = await fetch(cookieJar, "https://example.com");
|
||||||
|
|
||||||
// save the received cookies to disk
|
// save the received cookies to disk
|
||||||
cookieJar.save();
|
await cookieJar.save();
|
||||||
})();
|
})();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ...or without
|
||||||
|
```javascript
|
||||||
|
import {fetch, CookieJar} from "node-fetch-cookies";
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const cookieJar = new CookieJar();
|
||||||
|
|
||||||
|
// log in to some api
|
||||||
|
let response = await fetch(cookieJar, "https://example.com/api/login", {
|
||||||
|
method: "POST",
|
||||||
|
body: "credentials"
|
||||||
|
});
|
||||||
|
|
||||||
|
// do some requests you require login for
|
||||||
|
response = await fetch(cookieJar, "https://example.com/api/admin/drop-all-databases");
|
||||||
|
|
||||||
|
// and optionally log out again
|
||||||
|
response = await fetch(cookieJar, "https://example.com/api/logout");
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
### fetch(cookieJar, url, options)
|
### async fetch(cookieJar, url[, options])
|
||||||
- `cookieJar` A [CookieJar](#class-cookiejar) instance, an array of CookieJar instances or null, if you don't want to send or store cookies.
|
- `cookieJar` A [CookieJar](#class-cookiejar) instance, an array of CookieJar instances or null, if you don't want to send or store cookies.
|
||||||
- `url` and `options` as in https://github.com/bitinn/node-fetch#fetchurl-options
|
- `url` and `options` as in https://github.com/bitinn/node-fetch#fetchurl-options
|
||||||
|
|
||||||
|
Returns a Promise resolving to a [Response](https://github.com/bitinn/node-fetch#class-response) instance on success.
|
||||||
|
|
||||||
### Class: CookieJar
|
### Class: CookieJar
|
||||||
A class that stores cookies.
|
A class that stores cookies.
|
||||||
|
@ -31,27 +61,53 @@ A class that stores cookies.
|
||||||
#### Properties
|
#### Properties
|
||||||
- `flags` The read/write flags as specified below.
|
- `flags` The read/write flags as specified below.
|
||||||
- `file` The path of the cookie jar on the disk.
|
- `file` The path of the cookie jar on the disk.
|
||||||
- `cookies` A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) mapping cookie names to their properties.
|
- `cookies` A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) mapping hostnames to maps, which map cookie names to the respective [Cookie](#class-cookie) instance.
|
||||||
|
|
||||||
#### new CookieJar(flags[, file, cookies])
|
#### new CookieJar([file, flags = `rw`, cookies])
|
||||||
- `flags` A string specifying whether cookies should be read and/or written from/to the jar when passing it as parameter to [fetch](#fetchcookiejar-url-options).
|
- `file` An optional string containing a relative or absolute path to the file on the disk to use.
|
||||||
|
- `flags` An optional string specifying whether cookies should be read and/or written from/to the jar when passing it as parameter to [fetch](#fetchcookiejar-url-options). Default: `rw`
|
||||||
- `r`: only read from this jar
|
- `r`: only read from this jar
|
||||||
- `w`: only write to this jar
|
- `w`: only write to this jar
|
||||||
- `rw` or `wr`: read/write from/to this jar
|
- `rw` or `wr`: read/write from/to this jar
|
||||||
- `file` An optional string containing a relative or absolute path to the file on the disk to use.
|
|
||||||
- `cookies` An optional initializer for the cookie jar - either an array of [Cookie](#class-cookie) instances or a single Cookie instance.
|
- `cookies` An optional initializer for the cookie jar - either an array of [Cookie](#class-cookie) instances or a single Cookie instance.
|
||||||
|
|
||||||
#### addCookie(cookie[, url])
|
#### addCookie(cookie[, fromURL])
|
||||||
Adds a cookie to the jar.
|
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 the string received from a website. In this case `url` should be specified.
|
- `cookie` A [Cookie](#class-cookie) instance to add to the cookie jar.
|
||||||
- `url` The url a cookie has been received from.
|
Alternatively this can also be a string, for example a serialized cookie received from a website.
|
||||||
|
In this case `fromURL` must be specified.
|
||||||
|
- `fromURL` The url a cookie has been received from.
|
||||||
|
|
||||||
#### forEach(callback)
|
Returns `true` if the cookie has been added successfully. Returns `false` otherwise.
|
||||||
Just a wrapper for `CookieJar.cookies.forEach(callback)`.
|
If the parser throws a [CookieParseError](#class-cookieparseerror) it will be caught and a warning will be printed to console.
|
||||||
|
|
||||||
#### save()
|
#### domains()
|
||||||
Saves the cookie jar to disk. Only non-expired cookies are saved.
|
Returns an iterator over all domains currently stored cookies for.
|
||||||
|
|
||||||
|
#### *cookiesDomain(domain)
|
||||||
|
Returns an iterator over all cookies currently stored for `domain`.
|
||||||
|
|
||||||
|
#### *cookiesValid(withSession)
|
||||||
|
Returns an iterator over all valid (non-expired) cookies.
|
||||||
|
- `withSession`: A boolean. Iterator will include session cookies if set to `true`.
|
||||||
|
|
||||||
|
#### *cookiesAll()
|
||||||
|
Returns an iterator over all cookies currently stored.
|
||||||
|
|
||||||
|
#### *cookiesValidForRequest(requestURL)
|
||||||
|
Returns an iterator over all cookies valid for a request to `url`.
|
||||||
|
|
||||||
|
#### deleteExpired(sessionEnded)
|
||||||
|
Removes all expired cookies from the jar.
|
||||||
|
- `sessionEnded`: A boolean. Also removes session cookies if set to `true`.
|
||||||
|
|
||||||
|
#### async load([file = this.file])
|
||||||
|
Reads cookies from `file` on the disk and adds the contained cookies.
|
||||||
|
- `file`: Path to the file where the cookies should be saved. Default: `this.file`, the file that has been passed to the constructor.
|
||||||
|
|
||||||
|
#### async save([file = this.file])
|
||||||
|
Saves the cookie jar to `file` on the disk. Only non-expired non-session cookies are saved.
|
||||||
|
- `file`: Path to the file where the cookies should be saved. Default: `this.file`, the file that has been passed to the constructor.
|
||||||
|
|
||||||
### Class: Cookie
|
### Class: Cookie
|
||||||
An abstract representation of a cookie.
|
An abstract representation of a cookie.
|
||||||
|
@ -59,27 +115,46 @@ An abstract representation of a cookie.
|
||||||
#### Properties
|
#### Properties
|
||||||
- `name` The identifier of the cookie.
|
- `name` The identifier of the cookie.
|
||||||
- `value` The value of the cookie.
|
- `value` The value of the cookie.
|
||||||
- `expiry` A [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object of the cookies expiry date.
|
- `expiry` A [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object of the cookies expiry date or `null`, if the cookie expires with the session.
|
||||||
- `domain` The domain the cookie is valid for.
|
- `domain` The domain the cookie is valid for.
|
||||||
- `path` The path the cookie is valid for.
|
- `path` The path the cookie is valid for.
|
||||||
- `secure` A boolean value representing the cookie's secure attribute. If set the cookie will only be used for `https` requests.
|
- `secure` A boolean value representing the cookie's secure attribute. If set the cookie will only be used for `https` requests.
|
||||||
- `subdomains` A boolean value specifying whether the cookie should be used for subdomains of the domain or not.
|
- `subdomains` A boolean value specifying whether the cookie should be used for requests to subdomains of `domain` or not.
|
||||||
|
|
||||||
#### new Cookie(cookie, url)
|
#### new Cookie(str, requestURL)
|
||||||
- `cookie` The string representation of a cookie as send by a webserver.
|
Creates a cookie instance from the string representation of a cookie as send by a webserver.
|
||||||
|
- `str` The string representation of a cookie.
|
||||||
- `url` The url the cookie has been received from.
|
- `url` The url the cookie has been received from.
|
||||||
|
|
||||||
|
Will throw a `CookieParseError` if `str` 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.
|
||||||
|
|
||||||
#### serialize()
|
#### serialize()
|
||||||
Serializes the cookie, transforming it to `name=value` so it can be used in requests.
|
Serializes the cookie, transforming it to `name=value` so it can be used in requests.
|
||||||
|
|
||||||
#### hasExpired()
|
#### hasExpired(sessionEnded)
|
||||||
Returns whether the cookie has expired or not.
|
Returns whether the cookie has expired or not.
|
||||||
|
- `sessionEnded`: A boolean that specifies whether the current session has ended, meaning if set to `true`, the function will return `true` for session cookies.
|
||||||
|
|
||||||
|
#### isValidForRequest(requestURL)
|
||||||
|
Returns whether the cookie is valid for a request to `url`.
|
||||||
|
|
||||||
|
### Class: CookieParseError
|
||||||
|
The Error that is thrown when the cookie parser located in the constructor of the [Cookie](#class-cookie) class is unable to parse the input.
|
||||||
|
|
||||||
|
|
||||||
|
## 1.3.0 Breaking API Changes
|
||||||
|
- `new CookieJar(flags, file, cookies)` has been changed to `new CookieJar(file, flags = "rw", cookies)`.
|
||||||
|
`new CookieJar("rw")` can now be written as `new CookieJar()`, `new CookieJar("rw", "jar.json")` can now be written as `new CookieJar("jar.json")`.
|
||||||
|
This change has been introduced to simplify the usage of this library, since `rw` is used for `flags` in most cases anyways.
|
||||||
|
- `CookieJar.addFromFile(file)` has been renamed to the async function `async CookieJar.load([file = this.file])`, which uses the fsPromises API for non-blocking cookie loading.
|
||||||
|
The default value for `file` is the file passed to the constructor.
|
||||||
|
- `CookieJar.save(file)` was moved to `async CookieJar.save([file = this.file])` now also uses the fsPromises API.
|
||||||
|
- `new CookieJar()` now doesn't load cookies from the specified file anymore. To do so, call `await CookieJar.load()` after creating the CookieJar.
|
||||||
|
**NOTE: `CookieJar.load()` will throw an error if the cookie jar doesn't exist or doesn't contain valid JSON!**
|
||||||
|
|
||||||
#### isValidForRequest(url)
|
|
||||||
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).
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "flumm-fetch-cookies",
|
"name": "flumm-fetch-cookies",
|
||||||
"version": "1.1.2",
|
"version": "1.3.3",
|
||||||
"description": "flumm-fetch wrapper that adds support for cookie-jars",
|
"description": "flumm-fetch wrapper that adds support for cookie-jars",
|
||||||
"main": "src/index.mjs",
|
"main": "src/index.mjs",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=11.14.0"
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"src/"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "node test/index.mjs"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -1,30 +1,46 @@
|
||||||
import Cookie from "./cookie.mjs";
|
import {promises as fs} from "fs";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
|
import Cookie from "./cookie.mjs";
|
||||||
|
import {paramError, CookieParseError} from "./errors.mjs";
|
||||||
|
|
||||||
export default class CookieJar {
|
export default class CookieJar {
|
||||||
constructor() {
|
constructor(file, flags = "rw", cookies) {
|
||||||
|
this.flags = flags;
|
||||||
|
this.file = file;
|
||||||
this.cookies = new Map();
|
this.cookies = new Map();
|
||||||
|
if(typeof this.flags !== "string")
|
||||||
|
throw paramError("First", "flags", "new CookieJar()", "string");
|
||||||
|
if(this.file && typeof this.file !== "string")
|
||||||
|
throw paramError("Second", "file", "new CookieJar()", "string");
|
||||||
|
if(Array.isArray(cookies)) {
|
||||||
|
if(!cookies.every(c => c instanceof Cookie))
|
||||||
|
throw paramError("Third", "cookies", "new CookieJar()", "[Cookie]");
|
||||||
|
cookies.forEach(cookie => this.addCookie(cookie));
|
||||||
|
}
|
||||||
|
else if(cookies instanceof Cookie)
|
||||||
|
this.addCookie(cookies);
|
||||||
|
else if(cookies)
|
||||||
|
throw paramError("Third", "cookies", "new CookieJar()", ["[Cookie]", "Cookie"]);
|
||||||
}
|
}
|
||||||
addCookie(c, fromURL) {
|
addCookie(cookie, fromURL) {
|
||||||
if(typeof c === "string") {
|
if(typeof cookie === "string") {
|
||||||
try {
|
try {
|
||||||
c = new Cookie(c, fromURL);
|
cookie = new Cookie(cookie, fromURL);
|
||||||
}
|
}
|
||||||
catch(error) {
|
catch(error) {
|
||||||
if(error.name === "CookieParseError") {
|
if(error instanceof CookieParseError) {
|
||||||
console.warn("Ignored cookie: " + c);
|
console.warn("Ignored cookie: " + cookie);
|
||||||
console.warn("Reason: " + error.message);
|
console.warn("Reason: " + error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else
|
throw error;
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(!(c instanceof Cookie))
|
else if(!(cookie instanceof Cookie))
|
||||||
throw new TypeError("First parameter is neither a string nor a cookie!");
|
throw paramError("First", "cookie", "CookieJar.addCookie()", ["string", "Cookie"]);
|
||||||
if(!this.cookies.get(c.domain))
|
if(!this.cookies.get(cookie.domain))
|
||||||
this.cookies.set(c.domain, new Map());
|
this.cookies.set(cookie.domain, new Map());
|
||||||
this.cookies.get(c.domain).set(c.name, c);
|
this.cookies.get(cookie.domain).set(cookie.name, cookie);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
domains() {
|
domains() {
|
||||||
|
@ -44,7 +60,7 @@ export default class CookieJar {
|
||||||
yield* this.cookiesDomain(domain);
|
yield* this.cookiesDomain(domain);
|
||||||
}
|
}
|
||||||
*cookiesValidForRequest(requestURL) {
|
*cookiesValidForRequest(requestURL) {
|
||||||
const namesYielded = [],
|
const namesYielded = new Set(),
|
||||||
domains = url
|
domains = url
|
||||||
.parse(requestURL)
|
.parse(requestURL)
|
||||||
.hostname
|
.hostname
|
||||||
|
@ -54,8 +70,8 @@ export default class CookieJar {
|
||||||
for(const domain of domains) {
|
for(const domain of domains) {
|
||||||
for(const cookie of this.cookiesDomain(domain)) {
|
for(const cookie of this.cookiesDomain(domain)) {
|
||||||
if(cookie.isValidForRequest(requestURL)
|
if(cookie.isValidForRequest(requestURL)
|
||||||
&& namesYielded.every(name => name !== cookie.name)) {
|
&& !namesYielded.has(cookie.name)) {
|
||||||
namesYielded.push(cookie.name);
|
namesYielded.add(cookie.name);
|
||||||
yield cookie;
|
yield cookie;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,4 +82,15 @@ export default class CookieJar {
|
||||||
this.cookies = new Map();
|
this.cookies = new Map();
|
||||||
validCookies.forEach(c => this.addCookie(c));
|
validCookies.forEach(c => this.addCookie(c));
|
||||||
}
|
}
|
||||||
|
async load(file = this.file) {
|
||||||
|
if(typeof file !== "string")
|
||||||
|
throw new Error("No file has been specified for this cookie jar!");
|
||||||
|
JSON.parse(await fs.readFile(file)).forEach(c => this.addCookie(Cookie.fromObject(c)));
|
||||||
|
}
|
||||||
|
async save(file = this.file) {
|
||||||
|
if(typeof file !== "string")
|
||||||
|
throw new Error("No file has been specified for this cookie jar!");
|
||||||
|
// only save cookies that haven't expired
|
||||||
|
await fs.writeFile(this.file, JSON.stringify([...this.cookiesValid(false)]));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import url from "url";
|
import url from "url";
|
||||||
|
import {paramError, CookieParseError} from "./errors.mjs";
|
||||||
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();
|
||||||
|
@ -37,12 +31,17 @@ const splitN = (str, sep, n) => {
|
||||||
export default class Cookie {
|
export default class Cookie {
|
||||||
constructor(str, requestURL) {
|
constructor(str, requestURL) {
|
||||||
if(typeof str !== "string")
|
if(typeof str !== "string")
|
||||||
throw new TypeError("First parameter is not a 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("; ");
|
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 CookieParseError("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);
|
||||||
|
|
||||||
|
@ -56,7 +55,8 @@ export default class Cookie {
|
||||||
if(this.expiry) // max-age has precedence over expires
|
if(this.expiry) // max-age has precedence over expires
|
||||||
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)).toString() === "Invalid Date"
|
||||||
|
|| this.expiry.getTime() < 0)
|
||||||
throw new CookieParseError("Invalid value for Expires \"" + v + "\"!");
|
throw new CookieParseError("Invalid value for Expires \"" + v + "\"!");
|
||||||
}
|
}
|
||||||
else if(k === "max-age") {
|
else if(k === "max-age") {
|
||||||
|
@ -93,7 +93,7 @@ export default class Cookie {
|
||||||
|
|
||||||
if(this.name.toLowerCase().startsWith("__secure-") && (!this.secure || parsedURL.protocol !== "https:"))
|
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!");
|
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 !== "/")))
|
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 \"/\"!");
|
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
|
||||||
|
|
12
src/errors.mjs
Normal file
12
src/errors.mjs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export class CookieParseError extends Error {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.name = "CookieParseError";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function paramError(position, paramName, functionName, validTypes) {
|
||||||
|
validTypes = [validTypes].flat().map(t => "\"" + t + "\"");
|
||||||
|
validTypes = validTypes.slice(0, -1).join(", ") + (validTypes.length > 1 ? " or " : "") + validTypes.slice(-1);
|
||||||
|
return new TypeError(`${position} parameter "${paramName}" passed to "${functionName}" is not of type ${validTypes}!`);
|
||||||
|
};
|
|
@ -1,13 +1,12 @@
|
||||||
import fetch from "flumm-fetch";
|
import _fetch from "flumm-fetch";
|
||||||
import CookieJar from "./cookie-jar.mjs";
|
import CookieJar from "./cookie-jar.mjs";
|
||||||
import Cookie from "./cookie.mjs";
|
import Cookie from "./cookie.mjs";
|
||||||
|
|
||||||
const cookieJar = new CookieJar();
|
const cookieJar = new CookieJar();
|
||||||
|
|
||||||
export default async function cookieFetch(url, options) {
|
export default async function fetch(url, options) {
|
||||||
let cookies = "";
|
let cookies = "";
|
||||||
[...cookieJar.cookiesValidForRequest(url)]
|
[...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() + "; ");
|
.forEach(c => cookies += c.serialize() + "; ");
|
||||||
|
|
||||||
if(cookies) {
|
if(cookies) {
|
||||||
|
@ -30,3 +29,4 @@ export default async function cookieFetch(url, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export {cookieJar, CookieJar, Cookie};
|
export {cookieJar, CookieJar, Cookie};
|
||||||
|
|
||||||
|
|
285
test/cookie.mjs
Normal file
285
test/cookie.mjs
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
import Cookie from "../src/cookie.mjs";
|
||||||
|
import {CookieParseError} from "../src/errors.mjs";
|
||||||
|
|
||||||
|
export default Test => [
|
||||||
|
new Test("cookie parser", () => {
|
||||||
|
const inputs = [
|
||||||
|
[ // type error
|
||||||
|
123,
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[ // type error
|
||||||
|
"id=a3fWa",
|
||||||
|
123
|
||||||
|
],
|
||||||
|
[ // type error invalid url
|
||||||
|
"id=a3fWa",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[ // type error invalid url
|
||||||
|
"id=a3fWa",
|
||||||
|
"github.com"
|
||||||
|
],
|
||||||
|
// TODO: fix this test case, parser shouldn't allow it. it is currently ignored
|
||||||
|
[ // type error invalid url
|
||||||
|
"id=a3fWa",
|
||||||
|
"https:abc/abs"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid cookie name
|
||||||
|
"",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Oct 2015 07:28: GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Onv 2015 07:28:00 GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Oct 20151 07:28:00 GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=Wed, 32 Oct 2015 07:28:00 GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Oct 2015 25:28:00 GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Oct 2015 07:61:00 GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 UTC; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT+2; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=San, 21 Onv 2015 07:28:00 GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Expires=Wed, 31 Dec 1969 07:28:00 GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid value for expires
|
||||||
|
"id=a3fWa; Max-Age=121252a; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid key secur
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secur; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error invalid key HttpOly with value 2
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOly=2",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error not set via https
|
||||||
|
"__Secure-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly",
|
||||||
|
"http://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error secure not set
|
||||||
|
"__Secure-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error secure not set
|
||||||
|
"__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; HttpOnly; Path=/",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error not set via https
|
||||||
|
"__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Path=/",
|
||||||
|
"http://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error domain is set
|
||||||
|
"__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Domain=github.com; Path=/",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error path is not set
|
||||||
|
"__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error path is not equal to /
|
||||||
|
"__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Path=/lel/",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error domain is not a subdomain
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Domain=github.com",
|
||||||
|
"https://gist.github.com"
|
||||||
|
],
|
||||||
|
[ // cookie parse error domain is not a subdomain
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Domain=npmjs.com",
|
||||||
|
"https://gist.github.com"
|
||||||
|
],
|
||||||
|
[ // success
|
||||||
|
"__Secure-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // success
|
||||||
|
"__Host-id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Path=/",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // success
|
||||||
|
"__Host-id=a3fWa; Expires=Wed, 21 Nov 2099 20:28:33 GMT; Secure; HttpOnly; Path=/",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // success
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; Path=/lul/; Domain=.usercontent.github.com",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // success
|
||||||
|
"id=a3fWa; Expires=Wed, 21 Nov 2015 07:28:00 GMT; Secure; HttpOnly; SameSite=Strict; Path=/lul/; Domain=usercontent.github.com",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // success max-age takes precendence over expires
|
||||||
|
"id=a3fWa; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=1000",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
[ // success max-age takes precendence over expires
|
||||||
|
"id=a3fWa; Max-Age=1000; Expires=Thu, 01 Jan 1970 00:00:00 GMT",
|
||||||
|
"https://github.com"
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const catchErrorTest = (input, catchFnc) => {
|
||||||
|
try {
|
||||||
|
new Cookie(...input);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch(error) {
|
||||||
|
return catchFnc(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const catchErrorTypeMessageTest = (input, type, message) => catchErrorTest(input, e => e instanceof type && e.message === message);
|
||||||
|
|
||||||
|
const compareCookieProps = (input, expiryFnc, properties) => {
|
||||||
|
const cookie = new Cookie(...input);
|
||||||
|
return Object.entries(properties).every(([prop, value]) => cookie[prop] === value)
|
||||||
|
&& expiryFnc(cookie.expiry);
|
||||||
|
};
|
||||||
|
|
||||||
|
return inputs.slice(0, 3).every(input => catchErrorTest(input, e => e instanceof TypeError))
|
||||||
|
|
||||||
|
// cookies[4] is the test case that is ignored for now
|
||||||
|
|
||||||
|
&& catchErrorTypeMessageTest(inputs[5], CookieParseError, "Invalid cookie name \"\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[6], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 07:28: GMT\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[7], CookieParseError, "Invalid value for Expires \"Wed, 21 Onv 2015 07:28:00 GMT\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[8], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 20151 07:28:00 GMT\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[9], CookieParseError, "Invalid value for Expires \"Wed, 32 Oct 2015 07:28:00 GMT\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[10], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 25:28:00 GMT\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[11], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 07:61:00 GMT\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[12], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 07:28:00 UTC\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[13], CookieParseError, "Invalid value for Expires \"Wed, 21 Oct 2015 07:28:00 GMT+2\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[14], CookieParseError, "Invalid value for Expires \"San, 21 Onv 2015 07:28:00 GMT\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[15], CookieParseError, "Invalid value for Expires \"Wed, 31 Dec 1969 07:28:00 GMT\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[16], CookieParseError, "Invalid value for Max-Age \"121252a\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[17], CookieParseError, "Invalid key \"secur\" specified!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[18], CookieParseError, "Invalid key \"httpoly\" with value \"2\" specified!")
|
||||||
|
|
||||||
|
&& inputs.slice(19, 21).every(input => catchErrorTypeMessageTest(input, CookieParseError, "Cookie has \"__Secure-\" prefix but \"Secure\" isn't set or the cookie is not set via https!"))
|
||||||
|
|
||||||
|
&& inputs.slice(21, 26).every(input => catchErrorTypeMessageTest(input, 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 \"/\"!"))
|
||||||
|
|
||||||
|
&& catchErrorTypeMessageTest(inputs[26], CookieParseError, "Invalid value for Domain \"github.com\": cookie was received from \"gist.github.com\"!")
|
||||||
|
&& catchErrorTypeMessageTest(inputs[27], CookieParseError, "Invalid value for Domain \"npmjs.com\": cookie was received from \"gist.github.com\"!")
|
||||||
|
|
||||||
|
&& compareCookieProps(
|
||||||
|
inputs[28],
|
||||||
|
exp => exp.getTime() === new Date("Wed, 21 Nov 2015 07:28:00 GMT").getTime(),
|
||||||
|
{
|
||||||
|
name: "__Secure-id",
|
||||||
|
value: "a3fWa",
|
||||||
|
secure: true,
|
||||||
|
domain: "github.com",
|
||||||
|
subdomains: false,
|
||||||
|
path: "/"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
&& compareCookieProps(
|
||||||
|
inputs[29],
|
||||||
|
exp => exp.getTime() === new Date("Wed, 21 Nov 2015 07:28:00 GMT").getTime(),
|
||||||
|
{
|
||||||
|
name: "__Host-id",
|
||||||
|
value: "a3fWa",
|
||||||
|
secure: true,
|
||||||
|
domain: "github.com",
|
||||||
|
subdomains: false,
|
||||||
|
path: "/"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
&& compareCookieProps(
|
||||||
|
inputs[30],
|
||||||
|
exp => exp.getTime() === new Date("Wed, 21 Nov 2099 20:28:33 GMT").getTime(),
|
||||||
|
{
|
||||||
|
name: "__Host-id",
|
||||||
|
value: "a3fWa",
|
||||||
|
secure: true,
|
||||||
|
domain: "github.com",
|
||||||
|
subdomains: false,
|
||||||
|
path: "/"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
&& compareCookieProps(
|
||||||
|
inputs[31],
|
||||||
|
exp => exp.getTime() === new Date("Wed, 21 Nov 2015 07:28:00 GMT").getTime(),
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
value: "a3fWa",
|
||||||
|
secure: true,
|
||||||
|
domain: "usercontent.github.com",
|
||||||
|
subdomains: true,
|
||||||
|
path: "/lul/"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
&& compareCookieProps(
|
||||||
|
inputs[32],
|
||||||
|
exp => exp.getTime() === new Date("Wed, 21 Nov 2015 07:28:00 GMT").getTime(),
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
value: "a3fWa",
|
||||||
|
secure: true,
|
||||||
|
domain: "usercontent.github.com",
|
||||||
|
subdomains: true,
|
||||||
|
path: "/lul/"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
&& compareCookieProps(
|
||||||
|
inputs[33],
|
||||||
|
exp => exp.getTime() > new Date("Thu, 01 Jan 1970 00:00:00 GMT").getTime(),
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
value: "a3fWa",
|
||||||
|
secure: false,
|
||||||
|
domain: "github.com",
|
||||||
|
subdomains: false,
|
||||||
|
path: "/"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
&& compareCookieProps(
|
||||||
|
inputs[34],
|
||||||
|
exp => exp.getTime() > new Date("Thu, 01 Jan 1970 00:00:00 GMT").getTime(),
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
value: "a3fWa",
|
||||||
|
secure: false,
|
||||||
|
domain: "github.com",
|
||||||
|
subdomains: false,
|
||||||
|
path: "/"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
];
|
27
test/errors.mjs
Normal file
27
test/errors.mjs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import {CookieParseError, paramError} from "../src/errors.mjs";
|
||||||
|
|
||||||
|
export default Test => [
|
||||||
|
new Test("function paramError", () => {
|
||||||
|
const position = "something";
|
||||||
|
const paramName = "some_param";
|
||||||
|
const functionName = "some_func";
|
||||||
|
const validTypes = ["lol", "lel", "lul", "lal"];
|
||||||
|
const errors = [
|
||||||
|
paramError(position, paramName, functionName, validTypes[0]),
|
||||||
|
paramError(position, paramName, functionName, validTypes.slice(0, 2)),
|
||||||
|
paramError(position, paramName, functionName, validTypes)
|
||||||
|
];
|
||||||
|
return errors.every(e => e instanceof TypeError)
|
||||||
|
&& errors.every(e => e.name === "TypeError")
|
||||||
|
&& errors[0].message === "something parameter \"some_param\" passed to \"some_func\" is not of type \"lol\"!"
|
||||||
|
&& errors[1].message === "something parameter \"some_param\" passed to \"some_func\" is not of type \"lol\" or \"lel\"!"
|
||||||
|
&& errors[2].message === "something parameter \"some_param\" passed to \"some_func\" is not of type \"lol\", \"lel\", \"lul\" or \"lal\"!";
|
||||||
|
}),
|
||||||
|
new Test("class CookieParseError", () => {
|
||||||
|
const message = "this is a test error";
|
||||||
|
const error = new CookieParseError(message);
|
||||||
|
return error instanceof CookieParseError
|
||||||
|
&& error.name === "CookieParseError"
|
||||||
|
&& error.message === message;
|
||||||
|
})
|
||||||
|
];
|
45
test/index.mjs
Normal file
45
test/index.mjs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import cookie from "./cookie.mjs";
|
||||||
|
import errors from "./errors.mjs";
|
||||||
|
|
||||||
|
class Test {
|
||||||
|
constructor(name, fnc) {
|
||||||
|
this.name = name
|
||||||
|
this.fnc = fnc
|
||||||
|
}
|
||||||
|
async runTest() {
|
||||||
|
return this.fnc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
cookie,
|
||||||
|
errors
|
||||||
|
].flatMap(t => t(Test));
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
console.log("running tests...");
|
||||||
|
const testResults = await Promise.all(tests.map(async t => {
|
||||||
|
try {
|
||||||
|
t.result = await t.runTest();
|
||||||
|
if(typeof t.result !== "boolean") {
|
||||||
|
t.result = false;
|
||||||
|
console.error("test did not return a boolean: " + t.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(error) {
|
||||||
|
console.error("uncaught error in test: " + t.name + "\n", error);
|
||||||
|
t.result = false;
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}));
|
||||||
|
testResults.forEach(t => {
|
||||||
|
if(t.result)
|
||||||
|
console.info("✔ " + t.name);
|
||||||
|
else
|
||||||
|
console.warn("✘ " + t.name);
|
||||||
|
});
|
||||||
|
const succeededTests = testResults.map(t => t.result).reduce((a, b) => a + b);
|
||||||
|
const success = succeededTests === testResults.length;
|
||||||
|
(success ? console.info : console.warn)((success ? "✔" : "✘") + " " + succeededTests + "/" + testResults.length + " tests successful");
|
||||||
|
!success && process.exit(1);
|
||||||
|
})();
|
Loading…
Reference in New Issue
Block a user