Skip to content

Commit dc67354

Browse files
glynnbirdGlynn Bird
andauthored
bring cookie handling in-house. Fixes issue #324 (#325)
* bring cookie handling in-house. Fixes issue #324 Co-authored-by: Glynn Bird <glynnbird@apache.org>
1 parent 4924ddd commit dc67354

File tree

5 files changed

+603
-174
lines changed

5 files changed

+603
-174
lines changed

lib/cookie.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
const { URL } = require('url')
2+
3+
// a simple cookie jar
4+
class CookieJar {
5+
// create new empty cookie jar
6+
constructor () {
7+
this.jar = []
8+
}
9+
10+
// remove expired cookies
11+
clean () {
12+
const now = new Date().getTime()
13+
for (let i = 0; i < this.jar.length; i++) {
14+
const c = this.jar[i]
15+
if (c.ts < now) {
16+
this.jar.splice(i, 1)
17+
i--
18+
}
19+
}
20+
}
21+
22+
// add a cookie to the jar
23+
add (cookie, url) {
24+
// see if we have this cookie already
25+
const oldCookieIndex = this.findByName(url, cookie.name)
26+
27+
// if we do, update it
28+
if (oldCookieIndex >= 0) {
29+
// update existing cookie
30+
this.jar[oldCookieIndex].value = cookie.value
31+
this.jar[oldCookieIndex].expires = cookie.expires
32+
this.jar[oldCookieIndex].ts = new Date(cookie.expires).getTime()
33+
} else {
34+
// otherwise, just add it
35+
this.jar.push(cookie)
36+
}
37+
}
38+
39+
// locate a cookie by name & url
40+
findByName (url, name) {
41+
this.clean()
42+
const now = new Date().getTime()
43+
const parsedURL = new URL(url)
44+
for (let i = 0; i < this.jar.length; i++) {
45+
const c = this.jar[i]
46+
if (c.origin === parsedURL.origin &&
47+
c.name === name &&
48+
c.ts >= now) {
49+
return i
50+
}
51+
}
52+
return -1
53+
}
54+
55+
// get a list of cookies to send for a supplied URL
56+
getCookieString (url) {
57+
let i
58+
// clean up deceased cookies
59+
this.clean()
60+
61+
// find cookies that match the url
62+
const now = new Date().getTime()
63+
const parsedURL = new URL(url)
64+
const retval = []
65+
for (i = 0; i < this.jar.length; i++) {
66+
const c = this.jar[i]
67+
// if match domain name and timestamp
68+
if ((c.origin === parsedURL.origin ||
69+
(c.domain && parsedURL.hostname.endsWith(c.domain))) &&
70+
c.ts >= now) {
71+
// if cookie has httponly flag and this is not http(s), ignore
72+
if (c.httponly && !['http:', 'https:'].includes(parsedURL.protocol)) {
73+
continue
74+
}
75+
76+
// if cookie has a path and it doesn't match incoming url, ignore
77+
if (c.path && !parsedURL.pathname.startsWith(c.path)) {
78+
continue
79+
}
80+
81+
// if cookie has a secure flag and the transport isn't secure, ignore
82+
if (c.secure && parsedURL.protocol !== 'https:') {
83+
continue
84+
}
85+
86+
// add to list of returned cookies
87+
retval.push(c.value)
88+
}
89+
}
90+
// if we've got cookies to return
91+
if (retval.length > 0) {
92+
// join them with semi-colons
93+
return retval.join('; ')
94+
} else {
95+
// otherwise a blank string
96+
return ''
97+
}
98+
}
99+
100+
// parse a 'set-cookie' header of the form:
101+
// AuthSession=YWRtaW46NjM5ODgzQ0Y6TuB66MczvkZ7axEJq6Fz0gOdhKY; Version=1; Expires=Tue, 13-Dec-2022 13:54:19 GMT; Max-Age=60; Path=/; HttpOnly
102+
parse (h, url) {
103+
const parsedURL = new URL(url)
104+
105+
// split components by ; and remove whitespace
106+
const bits = h.split(';').map(s => s.trim())
107+
108+
// extract the cookie's value from the start of the string
109+
const cookieValue = bits.shift()
110+
111+
// start a cookie object
112+
const cookie = {
113+
name: cookieValue.split('=')[0], // the first part of the value
114+
origin: parsedURL.origin,
115+
pathname: parsedURL.pathname,
116+
protocol: parsedURL.protocol
117+
}
118+
bits.forEach((e) => {
119+
const lr = e.split('=')
120+
cookie[lr[0].toLowerCase()] = lr[1] || true
121+
})
122+
// calculate expiry timestamp
123+
cookie.ts = new Date(cookie.expires).getTime()
124+
cookie.value = cookieValue
125+
this.add(cookie, url)
126+
}
127+
}
128+
129+
module.exports = CookieJar

lib/nano.js

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,20 @@
1010
// License for the specific language governing permissions and limitations under
1111
// the License.
1212

13-
const { HttpsCookieAgent, HttpCookieAgent } = require('http-cookie-agent/http')
1413
const { URL } = require('url')
14+
const http = require('http')
15+
const https = require('https')
1516
const assert = require('assert')
1617
const querystring = require('qs')
1718
const axios = require('axios')
18-
const { CookieJar } = require('tough-cookie')
19-
const cookieJar = new CookieJar()
2019
const stream = require('stream')
2120
const pkg = require('../package.json')
22-
const AGENT_DEFAULTS = { cookies: { jar: cookieJar }, keepAlive: true, maxSockets: 50, keepAliveMsecs: 30000 }
21+
const AGENT_DEFAULTS = { keepAlive: true, maxSockets: 50, keepAliveMsecs: 30000 }
22+
const defaultHttpAgent = new http.Agent(AGENT_DEFAULTS)
23+
const defaultHttpsAgent = new https.Agent(AGENT_DEFAULTS)
2324
const SCRUBBED_STR = 'XXXXXX'
24-
const defaultHttpAgent = new HttpCookieAgent(AGENT_DEFAULTS)
25-
const defaultHttpsAgent = new HttpsCookieAgent(AGENT_DEFAULTS)
2625
const ChangesReader = require('./changesreader.js')
26+
const CookieJar = require('./cookie.js')
2727
const MultiPartFactory = require('./multipart.js')
2828

2929
function isEmpty (val) {
@@ -77,6 +77,9 @@ module.exports = exports = function dbScope (cfg) {
7777
const log = typeof cfg.log === 'function' ? cfg.log : dummyLogger
7878
const parseUrl = 'parseUrl' in cfg ? cfg.parseUrl : true
7979

80+
// create cookieJar for this Nano
81+
cfg.cookieJar = new CookieJar()
82+
8083
function maybeExtractDatabaseComponent () {
8184
if (!parseUrl) {
8285
return
@@ -123,6 +126,16 @@ module.exports = exports = function dbScope (cfg) {
123126
let body = response.data
124127
response.statusCode = statusCode
125128

129+
// cookie parsing
130+
if (response.headers) {
131+
const h = response.headers['set-cookie']
132+
if (h && h.length) {
133+
h.forEach((header) => {
134+
cfg.cookieJar.parse(header, req.url)
135+
})
136+
}
137+
}
138+
126139
// let parsed
127140
const responseHeaders = Object.assign({
128141
uri: scrubURL(req.url),
@@ -282,7 +295,6 @@ module.exports = exports = function dbScope (cfg) {
282295
}
283296

284297
if (isJar) {
285-
req.jar = cookieJar
286298
req.withCredentials = true
287299
}
288300

@@ -350,6 +362,12 @@ module.exports = exports = function dbScope (cfg) {
350362
req.qs = qs
351363
}
352364

365+
// add any cookies for this domain
366+
const cookie = cfg.cookieJar.getCookieString(req.uri)
367+
if (cookie) {
368+
req.headers.cookie = cookie
369+
}
370+
353371
if (opts.body) {
354372
if (Buffer.isBuffer(opts.body) || opts.dontStringify) {
355373
req.body = opts.body
@@ -375,8 +393,6 @@ module.exports = exports = function dbScope (cfg) {
375393
// ?drilldown=["author","Dickens"]&drilldown=["publisher","Penguin"]
376394
req.qsStringifyOptions = { arrayFormat: 'repeat' }
377395

378-
cfg.cookies = cookieJar.getCookiesSync(cfg.url)
379-
380396
// This where the HTTP request is made.
381397
// Nano used to use the now-deprecated "request" library but now we're going to
382398
// use axios, so let's modify the "req" object to suit axios
@@ -409,8 +425,6 @@ module.exports = exports = function dbScope (cfg) {
409425
// add http agents
410426
req.httpAgent = cfg.requestDefaults.agent || defaultHttpAgent
411427
req.httpsAgent = cfg.requestDefaults.agent || defaultHttpsAgent
412-
req.httpAgent.jar = req.httpAgent.jar ? req.httpAgent.jar : cookieJar
413-
req.httpsAgent.jar = req.httpsAgent.jar ? req.httpsAgent.jar : cookieJar
414428
const ax = axios.create({
415429
httpAgent: req.httpAgent,
416430
httpsAgent: req.httpsAgent

0 commit comments

Comments
 (0)