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 14:10:30 UTC

[couchdb-nano] 01/01: bring cookie handling in-house. Fixes issue #324

This is an automated email from the ASF dual-hosted git repository.

glynnbird pushed a commit to branch cookieparsing
in repository https://gitbox.apache.org/repos/asf/couchdb-nano.git

commit 87f7c3e5fb5ca17fd558fd5c3f09a7b87f147e29
Author: Glynn Bird <gl...@apache.org>
AuthorDate: Thu Jan 26 14:10:14 2023 +0000

    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(-)

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..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, '')
+})