Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bring cookie handling in-house. Fixes issue #324 #325

Merged
merged 2 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions lib/cookie.js
Original file line number Diff line number Diff line change
@@ -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
36 changes: 25 additions & 11 deletions lib/nano.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -282,7 +295,6 @@ module.exports = exports = function dbScope (cfg) {
}

if (isJar) {
req.jar = cookieJar
req.withCredentials = true
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading