From 87f7c3e5fb5ca17fd558fd5c3f09a7b87f147e29 Mon Sep 17 00:00:00 2001 From: Glynn Bird Date: Thu, 26 Jan 2023 14:10:14 +0000 Subject: [PATCH 1/2] bring cookie handling in-house. Fixes issue #324 --- lib/cookie.js | 129 +++++++++++++ lib/nano.js | 36 ++-- package-lock.json | 171 ++--------------- package.json | 5 +- test/cookie.test.js | 436 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 603 insertions(+), 174 deletions(-) create mode 100644 lib/cookie.js create mode 100644 test/cookie.test.js diff --git a/lib/cookie.js b/lib/cookie.js new file mode 100644 index 0000000..d18a4ce --- /dev/null +++ b/lib/cookie.js @@ -0,0 +1,129 @@ +const { URL } = require('url') + +// a simple cookie jar +class CookieJar { + // create new empty cookie jar + constructor () { + this.jar = [] + } + + // remove expired cookies + clean () { + const now = new Date().getTime() + for (let i = 0; i < this.jar.length; i++) { + const c = this.jar[i] + if (c.ts < now) { + this.jar.splice(i, 1) + i-- + } + } + } + + // add a cookie to the jar + add (cookie, url) { + // see if we have this cookie already + const oldCookieIndex = this.findByName(url, cookie.name) + + // if we do, update it + if (oldCookieIndex >= 0) { + // update existing cookie + this.jar[oldCookieIndex].value = cookie.value + this.jar[oldCookieIndex].expires = cookie.expires + this.jar[oldCookieIndex].ts = new Date(cookie.expires).getTime() + } else { + // otherwise, just add it + this.jar.push(cookie) + } + } + + // locate a cookie by name & url + findByName (url, name) { + this.clean() + const now = new Date().getTime() + const parsedURL = new URL(url) + for (let i = 0; i < this.jar.length; i++) { + const c = this.jar[i] + if (c.origin === parsedURL.origin && + c.name === name && + c.ts >= now) { + return i + } + } + return -1 + } + + // get a list of cookies to send for a supplied URL + getCookieString (url) { + let i + // clean up deceased cookies + this.clean() + + // find cookies that match the url + const now = new Date().getTime() + const parsedURL = new URL(url) + const retval = [] + for (i = 0; i < this.jar.length; i++) { + const c = this.jar[i] + // if match domain name and timestamp + if ((c.origin === parsedURL.origin || + (c.domain && parsedURL.hostname.endsWith(c.domain))) && + c.ts >= now) { + // if cookie has httponly flag and this is not http(s), ignore + if (c.httponly && !['http:', 'https:'].includes(parsedURL.protocol)) { + continue + } + + // if cookie has a path and it doesn't match incoming url, ignore + if (c.path && !parsedURL.pathname.startsWith(c.path)) { + continue + } + + // if cookie has a secure flag and the transport isn't secure, ignore + if (c.secure && parsedURL.protocol !== 'https:') { + continue + } + + // add to list of returned cookies + retval.push(c.value) + } + } + // if we've got cookies to return + if (retval.length > 0) { + // join them with semi-colons + return retval.join('; ') + } else { + // otherwise a blank string + return '' + } + } + + // parse a 'set-cookie' header of the form: + // AuthSession=YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY; Version=1; Expires=Tue, 13-Dec-2022 13:54:19 GMT; Max-Age=60; Path=/; HttpOnly + parse (h, url) { + const parsedURL = new URL(url) + + // split components by ; and remove whitespace + const bits = h.split(';').map(s => s.trim()) + + // extract the cookie's value from the start of the string + const cookieValue = bits.shift() + + // start a cookie object + const cookie = { + name: cookieValue.split('=')[0], // the first part of the value + origin: parsedURL.origin, + pathname: parsedURL.pathname, + protocol: parsedURL.protocol + } + bits.forEach((e) => { + const lr = e.split('=') + cookie[lr[0].toLowerCase()] = lr[1] || true + }) + // calculate expiry timestamp + cookie.ts = new Date(cookie.expires).getTime() + cookie.value = cookieValue + this.add(cookie, url) + } +} + +module.exports = CookieJar diff --git a/lib/nano.js b/lib/nano.js index 6f76199..d88de73 100644 --- a/lib/nano.js +++ b/lib/nano.js @@ -10,20 +10,20 @@ // License for the specific language governing permissions and limitations under // the License. -const { HttpsCookieAgent, HttpCookieAgent } = require('http-cookie-agent/http') const { URL } = require('url') +const http = require('http') +const https = require('https') const assert = require('assert') const querystring = require('qs') const axios = require('axios') -const { CookieJar } = require('tough-cookie') -const cookieJar = new CookieJar() const stream = require('stream') const pkg = require('../package.json') -const AGENT_DEFAULTS = { cookies: { jar: cookieJar }, keepAlive: true, maxSockets: 50, keepAliveMsecs: 30000 } +const AGENT_DEFAULTS = { keepAlive: true, maxSockets: 50, keepAliveMsecs: 30000 } +const defaultHttpAgent = new http.Agent(AGENT_DEFAULTS) +const defaultHttpsAgent = new https.Agent(AGENT_DEFAULTS) const SCRUBBED_STR = 'XXXXXX' -const defaultHttpAgent = new HttpCookieAgent(AGENT_DEFAULTS) -const defaultHttpsAgent = new HttpsCookieAgent(AGENT_DEFAULTS) const ChangesReader = require('./changesreader.js') +const CookieJar = require('./cookie.js') const MultiPartFactory = require('./multipart.js') function isEmpty (val) { @@ -77,6 +77,9 @@ module.exports = exports = function dbScope (cfg) { const log = typeof cfg.log === 'function' ? cfg.log : dummyLogger const parseUrl = 'parseUrl' in cfg ? cfg.parseUrl : true + // create cookieJar for this Nano + cfg.cookieJar = new CookieJar() + function maybeExtractDatabaseComponent () { if (!parseUrl) { return @@ -123,6 +126,16 @@ module.exports = exports = function dbScope (cfg) { let body = response.data response.statusCode = statusCode + // cookie parsing + if (response.headers) { + const h = response.headers['set-cookie'] + if (h && h.length) { + h.forEach((header) => { + cfg.cookieJar.parse(header, req.url) + }) + } + } + // let parsed const responseHeaders = Object.assign({ uri: scrubURL(req.url), @@ -282,7 +295,6 @@ module.exports = exports = function dbScope (cfg) { } if (isJar) { - req.jar = cookieJar req.withCredentials = true } @@ -350,6 +362,12 @@ module.exports = exports = function dbScope (cfg) { req.qs = qs } + // add any cookies for this domain + const cookie = cfg.cookieJar.getCookieString(req.uri) + if (cookie) { + req.headers.cookie = cookie + } + if (opts.body) { if (Buffer.isBuffer(opts.body) || opts.dontStringify) { req.body = opts.body @@ -375,8 +393,6 @@ module.exports = exports = function dbScope (cfg) { // ?drilldown=["author","Dickens"]&drilldown=["publisher","Penguin"] req.qsStringifyOptions = { arrayFormat: 'repeat' } - cfg.cookies = cookieJar.getCookiesSync(cfg.url) - // This where the HTTP request is made. // Nano used to use the now-deprecated "request" library but now we're going to // use axios, so let's modify the "req" object to suit axios @@ -409,8 +425,6 @@ module.exports = exports = function dbScope (cfg) { // add http agents req.httpAgent = cfg.requestDefaults.agent || defaultHttpAgent req.httpsAgent = cfg.requestDefaults.agent || defaultHttpsAgent - req.httpAgent.jar = req.httpAgent.jar ? req.httpAgent.jar : cookieJar - req.httpsAgent.jar = req.httpsAgent.jar ? req.httpsAgent.jar : cookieJar const ax = axios.create({ httpAgent: req.httpAgent, httpsAgent: req.httpsAgent diff --git a/package-lock.json b/package-lock.json index c0ec08e..134989e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,17 @@ { "name": "nano", - "version": "10.1.0", + "version": "10.1.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nano", - "version": "10.1.0", + "version": "10.1.2", "license": "Apache-2.0", "dependencies": { - "@types/tough-cookie": "^4.0.2", "axios": "^1.2.2", - "http-cookie-agent": "^5.0.2", "node-abort-controller": "^3.0.1", - "qs": "^6.11.0", - "tough-cookie": "^4.1.2" + "qs": "^6.11.0" }, "devDependencies": { "@types/node": "^18.11.9", @@ -1230,11 +1227,6 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, - "node_modules/@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" - }, "node_modules/@types/yargs": { "version": "17.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", @@ -1271,17 +1263,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1787,6 +1768,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3017,33 +2999,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/http-cookie-agent": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-5.0.2.tgz", - "integrity": "sha512-BiBmZyIMGl5mLKmY7KH2uCVlcNUl1jexjdtWXFCUF4DFOrNZg1c5iPPTzWDzU7Ngfb6fB03DPpJQ80KQWmycsg==", - "dependencies": { - "agent-base": "^6.0.2" - }, - "engines": { - "node": ">=14.18.0 <15.0.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/3846masa" - }, - "peerDependencies": { - "deasync": "^0.1.26", - "tough-cookie": "^4.0.0", - "undici": "^5.11.0" - }, - "peerDependenciesMeta": { - "deasync": { - "optional": true - }, - "undici": { - "optional": true - } - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4348,7 +4303,8 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -4870,15 +4826,11 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, "engines": { "node": ">=6" } @@ -4897,11 +4849,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4966,11 +4913,6 @@ "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -5433,20 +5375,6 @@ "node": ">=8.0" } }, - "node_modules/tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -5541,14 +5469,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -5584,15 +5504,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -6697,11 +6608,6 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, - "@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" - }, "@types/yargs": { "version": "17.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", @@ -6730,14 +6636,6 @@ "dev": true, "requires": {} }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -7115,6 +7013,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -7980,14 +7879,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "http-cookie-agent": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-5.0.2.tgz", - "integrity": "sha512-BiBmZyIMGl5mLKmY7KH2uCVlcNUl1jexjdtWXFCUF4DFOrNZg1c5iPPTzWDzU7Ngfb6fB03DPpJQ80KQWmycsg==", - "requires": { - "agent-base": "^6.0.2" - } - }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -8962,7 +8853,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "natural-compare": { "version": "1.4.0", @@ -9351,15 +9243,11 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true }, "qs": { "version": "6.11.0", @@ -9369,11 +9257,6 @@ "side-channel": "^1.0.4" } }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9409,11 +9292,6 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -9728,17 +9606,6 @@ "is-number": "^7.0.0" } }, - "tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - } - }, "tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -9807,11 +9674,6 @@ "which-boxed-primitive": "^1.0.2" } }, - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" - }, "update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -9831,15 +9693,6 @@ "punycode": "^2.1.0" } }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", diff --git a/package.json b/package.json index 1f316ef..31ff979 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "Apache-2.0", "homepage": "http://github.com/apache/couchdb-nano", "repository": "http://github.com/apache/couchdb-nano", - "version": "10.1.1", + "version": "10.1.2", "author": "Apache CouchDB (http://couchdb.apache.org)", "keywords": [ "couchdb", @@ -17,11 +17,8 @@ "database" ], "dependencies": { - "http-cookie-agent": "^5.0.2", - "@types/tough-cookie": "^4.0.2", "axios": "^1.2.2", "qs": "^6.11.0", - "tough-cookie": "^4.1.2", "node-abort-controller": "^3.0.1" }, "devDependencies": { diff --git a/test/cookie.test.js b/test/cookie.test.js new file mode 100644 index 0000000..b2163a8 --- /dev/null +++ b/test/cookie.test.js @@ -0,0 +1,436 @@ +// Licensed under the Apache License, Version 2.0 (the 'License'); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +const assert = require('node:assert/strict') + +const CookieJar = require('../lib/cookie.js') + +test('should parse cookies correctly', () => { + const cj = new CookieJar() + const expiry = new Date().getTime() + 1000 * 60 + const expiryStr = new Date(expiry).toGMTString() + const n = 'AuthSession' + const v = `${n}=YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY` + const sc = `${v}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly` + const url = 'http://mydomain.com/_session' + cj.parse(sc, url) + assert.equal(cj.jar.length, 1) + const cookie = { + name: 'AuthSession', + origin: 'http://mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: v + } + assert.deepEqual(cj.jar[0], cookie) +}) + +test('should handle multiple cookies', () => { + const cj = new CookieJar() + const expiry = new Date().getTime() + 1000 * 60 + const expiryStr = new Date(expiry).toGMTString() + const n = 'AuthSession' + const v1 = 'YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY' + const v2 = 'YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY' + const v3 = 'YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY' + const sc1 = `${n}1=${v1}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly` + const sc2 = `${n}2=${v2}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly` + const sc3 = `${n}3=${v3}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly` + const url = 'http://mydomain.com/_session' + cj.parse(sc1, url) + cj.parse(sc2, url) + cj.parse(sc3, url) + assert.equal(cj.jar.length, 3) + let cookie = { + name: 'AuthSession1', + origin: 'http://mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: `AuthSession1=${v1}` + } + assert.deepEqual(cj.jar[0], cookie) + cookie = { + name: 'AuthSession2', + origin: 'http://mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: `AuthSession2=${v2}` + } + assert.deepEqual(cj.jar[1], cookie) + cookie = { + name: 'AuthSession3', + origin: 'http://mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: `AuthSession3=${v3}` + } + assert.deepEqual(cj.jar[2], cookie) +}) + +test('should handle multiple domains', () => { + const cj = new CookieJar() + const expiry = new Date().getTime() + 1000 * 60 + const expiryStr = new Date(expiry).toGMTString() + const n = 'AuthSession' + const v1 = 'gzQ0Y6TuB66MczYWRtaW46NjM5ODvkZ7axEJq6Fz0gOdhKY' + const v2 = 'YWRtaWzQ0Y6T46NjM5ODguB66MczvkZ7axEJq6Fz0gOdhKY' + const v3 = '46NjM5ODgYWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY' + const v4 = 'Y6TuB66MczvkZ7axY6TuBxzvkZ7ax46NjM5ODgYWRtaW46NjM5ODgzQ0EJq6Fz0gOdhKY' + const sc1 = `${n}1=${v1}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly` + const sc2 = `${n}2=${v2}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly` + const sc3 = `${n}3=${v3}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly` + const sc4 = `${n}4=${v4}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly` + const url1 = 'http://mydomain1.com/_session' + const url2 = 'http://mydomain2.com/_session' + const url3 = 'http://mydomain3.com/_session' + cj.parse(sc1, url1) + cj.parse(sc2, url2) + cj.parse(sc3, url3) + cj.parse(sc4, url3) + assert.equal(cj.jar.length, 4) + let cookie = { + name: 'AuthSession1', + origin: 'http://mydomain1.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: `AuthSession1=${v1}` + } + assert.deepEqual(cj.jar[0], cookie) + assert.deepEqual(cj.getCookieString(url1), cookie.value) + cookie = { + name: 'AuthSession2', + origin: 'http://mydomain2.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: `AuthSession2=${v2}` + } + assert.deepEqual(cj.jar[1], cookie) + assert.deepEqual(cj.getCookieString(url2), cookie.value) + const cookie1 = { + name: 'AuthSession3', + origin: 'http://mydomain3.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: `AuthSession3=${v3}` + } + assert.deepEqual(cj.jar[2], cookie1) + const cookie2 = { + name: 'AuthSession4', + origin: 'http://mydomain3.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: `AuthSession4=${v4}` + } + assert.deepEqual(cj.jar[3], cookie2) + // multiple cookies - 2 cookies for url3 + assert.equal(cj.getCookieString(url3), `${cookie1.value}; ${cookie2.value}`) + + // check we don't get a cookie for a subdomain + assert.equal(cj.getCookieString('http://sub.mydomain3.com'), '') +}) + +const sleep = async (ms) => { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms) + }) +} + +test('should expire cookies correctly', async () => { + const cj = new CookieJar() + const expiry = new Date().getTime() + 1000 * 4 + const expiryStr = new Date(expiry).toGMTString() + const n = 'AuthSession' + const v = `${n}=YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY` + const sc = `${v}; Version=1; Expires=${expiryStr}; Max-Age=5; Path=/; HttpOnly` + const url = 'http://mydomain.com/_session' + cj.parse(sc, url) + assert.equal(cj.jar.length, 1) + assert.notEqual(cj.getCookieString(url).length, 0) + await sleep(4000) + assert.equal(cj.getCookieString(url).length, 0) + assert.equal(cj.getCookieString(url).length, 0) +}) + +test('should respect path', () => { + const cj = new CookieJar() + const expiry = new Date().getTime() + 1000 * 60 + const expiryStr = new Date(expiry).toGMTString() + const n = 'AuthSession' + const v1 = `${n}1=YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY` + const sc1 = `${v1}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/my/path; HttpOnly` + const v2 = `${n}2=YczvkZ7axEJq6Fz0gOdhKYWRtaW46NjM5ODgzQ0Y6TuB66M` + const sc2 = `${v2}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly` + + const url = 'http://mydomain.com/_session' + cj.parse(sc1, url) + cj.parse(sc2, url) + assert.equal(cj.jar.length, 2) + const cookie1 = { + name: 'AuthSession1', + origin: 'http://mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/my/path', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: v1 + } + assert.deepEqual(cj.jar[0], cookie1) + const cookie2 = { + name: 'AuthSession2', + origin: 'http://mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr).getTime(), + value: v2 + } + assert.deepEqual(cj.jar[1], cookie2) + + // one cookies for path=/ + let cs = cj.getCookieString('http://mydomain.com/') + assert.equal(cs, `${cookie2.value}`) + // two cookies for path=/my/path + cs = cj.getCookieString('http://mydomain.com/my/path') + assert.equal(cs, `${cookie1.value}; ${cookie2.value}`) + // two cookies for path=/my/path/extra + cs = cj.getCookieString('http://mydomain.com/my/path/extra') + assert.equal(cs, `${cookie1.value}; ${cookie2.value}`) + // zero cookies for different domain + cs = cj.getCookieString('http://myotherdomain.com/my/path/extra') + assert.equal(cs, '') +}) + +test('should renew cookies', () => { + const cj = new CookieJar() + const n = 'AuthSession' + const expiry1 = new Date().getTime() + 1000 * 60 + const expiryStr1 = new Date(expiry1).toGMTString() + + const v1 = `${n}=YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY` + const sc1 = `${v1}; Version=1; Expires=${expiryStr1}; Max-Age=60; Path=/; HttpOnly` + + const expiry2 = new Date().getTime() + 1000 * 120 + const expiryStr2 = new Date(expiry2).toGMTString() + const v2 = `${n}=gOdhKYWRtaW46NjM5ODgzQ0Y6TuB66MYczvkZ7axEJq6Fz0` + const sc2 = `${v2}; Version=1; Expires=${expiryStr2}; Max-Age=60; Path=/; HttpOnly` + + const url = 'http://mydomain.com/_session' + + // parse first cookie string + cj.parse(sc1, url) + assert.equal(cj.jar.length, 1) + const cookie1 = { + name: 'AuthSession', + origin: 'http://mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr1, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr1).getTime(), + value: v1 + } + assert.deepEqual(cj.jar[0], cookie1) + + // then refresh the cookie + cj.parse(sc2, url) + const cookie2 = { + name: 'AuthSession', + origin: 'http://mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr2, + 'max-age': '60', + path: '/', + httponly: true, + ts: new Date(expiryStr2).getTime(), + value: v2 + } + + // ensure it updates the same cookie + assert.equal(cj.jar.length, 1) + assert.deepEqual(cj.jar[0], cookie2) +}) + +test('should send cookies to authorised subdomains', () => { + const cj = new CookieJar() + const expiry = new Date().getTime() + 1000 * 60 + const expiryStr = new Date(expiry).toGMTString() + const n = 'AuthSession' + const v = `${n}=YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY` + const sc = `${v}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly; Domain=.mydomain.com` + const url = 'http://test.mydomain.com/_session' + cj.parse(sc, url) + assert.equal(cj.jar.length, 1) + const cookie = { + name: 'AuthSession', + origin: 'http://test.mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + domain: '.mydomain.com', + ts: new Date(expiryStr).getTime(), + value: v + } + assert.deepEqual(cj.jar[0], cookie) + + // check we get a cookie for the same domain + let cs = cj.getCookieString('http://test.mydomain.com/my/path/extra') + assert.equal(cs, `${cookie.value}`) + + // check we get a cookie for the different domain + cs = cj.getCookieString('http://different.mydomain.com/my/path/extra') + assert.equal(cs, `${cookie.value}`) + cs = cj.getCookieString('http://sub.different.mydomain.com/my/path/extra') + assert.equal(cs, `${cookie.value}`) + + // check we get no cookies for the different domain + cs = cj.getCookieString('http://mydomain1.com/my/path/extra') + assert.equal(cs, '') +}) + +test('should not send http-only cookies to different protocol', () => { + const cj = new CookieJar() + const expiry = new Date().getTime() + 1000 * 60 + const expiryStr = new Date(expiry).toGMTString() + const n = 'AuthSession' + const v = `${n}=YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY` + const sc = `${v}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; HttpOnly; Domain=.mydomain.com` + const url = 'http://test.mydomain.com/_session' + cj.parse(sc, url) + assert.equal(cj.jar.length, 1) + const cookie = { + name: 'AuthSession', + origin: 'http://test.mydomain.com', + pathname: '/_session', + protocol: 'http:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + httponly: true, + domain: '.mydomain.com', + ts: new Date(expiryStr).getTime(), + value: v + } + assert.deepEqual(cj.jar[0], cookie) + + // check we get a cookie for the same domain (http) + let cs = cj.getCookieString('http://test.mydomain.com/my/path/extra') + assert.equal(cs, `${cookie.value}`) + + // check we get a cookie for the same domain (https) + cs = cj.getCookieString('https://test.mydomain.com/my/path/extra') + assert.equal(cs, `${cookie.value}`) + + // but not some other protocol + cs = cj.getCookieString('ws://test.mydomain.com/my/path/extra') + assert.equal(cs, '') +}) + +test('should not send secure-only cookies to http', () => { + const cj = new CookieJar() + const expiry = new Date().getTime() + 1000 * 60 + const expiryStr = new Date(expiry).toGMTString() + const n = 'AuthSession' + const v = `${n}=YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY` + const sc = `${v}; Version=1; Expires=${expiryStr}; Max-Age=60; Path=/; Secure; Domain=.mydomain.com` + const url = 'https://test.mydomain.com/_session' + cj.parse(sc, url) + assert.equal(cj.jar.length, 1) + const cookie = { + name: 'AuthSession', + origin: 'https://test.mydomain.com', + pathname: '/_session', + protocol: 'https:', + version: '1', + expires: expiryStr, + 'max-age': '60', + path: '/', + secure: true, + domain: '.mydomain.com', + ts: new Date(expiryStr).getTime(), + value: v + } + assert.deepEqual(cj.jar[0], cookie) + + // check we get a cookie for the same domain (http) + let cs = cj.getCookieString('https://test.mydomain.com/my/path/extra') + assert.equal(cs, `${cookie.value}`) + + // but not http + cs = cj.getCookieString('http://test.mydomain.com/my/path/extra') + assert.equal(cs, '') +}) From 4155cd637920315bf59fa6aa13d53b34387c833e Mon Sep 17 00:00:00 2001 From: Glynn Bird Date: Thu, 26 Jan 2023 14:26:01 +0000 Subject: [PATCH 2/2] old assert for node14 --- test/cookie.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cookie.test.js b/test/cookie.test.js index b2163a8..6df66fe 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -10,7 +10,7 @@ // License for the specific language governing permissions and limitations under // the License. -const assert = require('node:assert/strict') +const assert = require('assert') const CookieJar = require('../lib/cookie.js')