You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by gl...@apache.org on 2023/01/26 15:26:39 UTC
[couchdb-nano] branch main updated: bring cookie handling in-house. Fixes issue #324 (#325)
This is an automated email from the ASF dual-hosted git repository.
glynnbird pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb-nano.git
The following commit(s) were added to refs/heads/main by this push:
new dc67354 bring cookie handling in-house. Fixes issue #324 (#325)
dc67354 is described below
commit dc673542ca65998c5e170db95dde3a090bb9a4c9
Author: Glynn Bird <gl...@gmail.com>
AuthorDate: Thu Jan 26 15:26:33 2023 +0000
bring cookie handling in-house. Fixes issue #324 (#325)
* bring cookie handling in-house. Fixes issue #324
Co-authored-by: Glynn Bird <gl...@apache.org>
---
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(-)
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 <de...@couchdb.apache.org> (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..6df66fe
--- /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('assert')
+
+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, '')
+})