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

1.0.0 #48

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
node_modules
.DS_Store*
npm-debug.log
38 changes: 38 additions & 0 deletions lib/cookie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

module.exports = Cookie

function Cookie(name, value, attrs) {
value || (this.expires = new Date(0))

this.name = name
this.value = value || ""

for (var name in attrs) this[name] = attrs[name]
}

Cookie.prototype = {
path: "/",
expires: undefined,
domain: undefined,
httpOnly: true,
secure: false,
overwrite: true,

toString: function() {
return this.name + "=" + this.value
},

toHeader: function() {
var header = this.toString()

if (this.maxage) this.expires = new Date(Date.now() + this.maxage);

if (this.path ) header += "; path=" + this.path
if (this.expires ) header += "; expires=" + this.expires.toUTCString()
if (this.domain ) header += "; domain=" + this.domain
if (this.secure ) header += "; secure"
if (this.httpOnly ) header += "; httponly"

return header
}
}
219 changes: 112 additions & 107 deletions lib/cookies.js
Original file line number Diff line number Diff line change
@@ -1,143 +1,148 @@

var base64 = require('base64-url')
var Keygrip = require('keygrip')
var http = require('http')
var cache = {}

function Cookies(request, response, keys) {
if (!(this instanceof Cookies)) return new Cookies(request, response, keys)

this.request = request
this.response = response
if (keys) {
// array of key strings
if (Array.isArray(keys))
this.keys = new Keygrip(keys)
// any keygrip constructor to allow different versions
else if (keys.constructor && keys.constructor.name === 'Keygrip')
this.keys = keys
}
}

Cookies.prototype = {
get: function(name, opts) {
var sigName = name + ".sig"
, header, match, value, remote, data, index
, signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys
var Cookie = require('./cookie')
var utils = require('./utils')

header = this.request.headers["cookie"]
if (!header) return
var getPattern = utils.getPattern
var pushCookie = utils.pushCookie
var merge = utils.merge

match = header.match(getPattern(name))
if (!match) return
module.exports = function (options, keys) {
if (Array.isArray(options)
|| (options && options.constructor && options.constructor.name === 'Keygrip')) {
keys = options
options = {}
}

value = match[1]
if (!opts || !signed) return value
options = options || {}
keys = keys || options.keys
if (Array.isArray(keys)) keys = new Keygrip(keys)

remote = this.get(sigName)
if (!remote) return
function Cookies(req, res, next) {
if (!(this instanceof Cookies)) return new Cookies(req, res, next)

data = name + "=" + value
if (!this.keys) throw new Error('.keys required for signed cookies');
index = this.keys.index(data, remote)
this.req = req
this.res = res

if (index < 0) {
this.set(sigName, null, {path: "/", signed: false })
} else {
index && this.set(sigName, this.keys.sign(data), { signed: false })
return value
// middleware support
if (typeof next === 'function') {
req.cookies = res.cookies = this
next()
}
},
}

Cookies.prototype.get = function (name) {
var header = this.req.headers.cookie
if (!header) return
var match = header.match(getPattern(name))
if (!match) return
return match[1]
}

set: function(name, value, opts) {
var res = this.response
, req = this.request
, headers = res.getHeader("Set-Cookie") || []
, secure = req.connection.encrypted
, cookie = new Cookie(name, value, opts)
, signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys
Cookies.prototype.set = function (name, value, opts) {
opts = this.extend(opts)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this extend thing would be a little slow to use on each method, but perhaps that's not an issue.


var res = this.res
var headers = res.getHeader('Set-Cookie') || []
// node.js header sillyness
if (typeof headers == "string") headers = [headers]

if (!secure && opts && opts.secure) throw new Error("Cannot send secure cookie over unencrypted socket")

cookie.secure = secure
if (opts && "secure" in opts) cookie.secure = opts.secure
if (opts && "secureProxy" in opts) cookie.secure = opts.secureProxy
var cookie = new Cookie(name, value, opts)
headers = pushCookie(headers, cookie)

if (opts && signed) {
if (!this.keys) throw new Error('.keys required for signed cookies');
cookie.value = this.keys.sign(cookie.toString())
cookie.name += ".sig"
headers = pushCookie(headers, cookie)
}
res.setHeader('Set-Cookie', headers)
return this
}

Cookies.prototype.decode = function (name, buffer) {
var value = this.get(name)
if (!value) return

var buf = new Buffer(base64.unescape(value), 'base64')
if (buffer) return buf
return buf.toString('utf8')
}

var setHeader = res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader
setHeader.call(res, 'Set-Cookie', headers)
Cookies.prototype.encode = function (name, value, opts) {
opts = this.extend(opts)

var digest = base64.escape(new Buffer(value, 'utf8').toString('base64'))
this.set(name, digest, opts)
return this
}
}

function Cookie(name, value, attrs) {
value || (this.expires = new Date(0))
Cookies.prototype.unsign = function (name, opts) {
opts = this.extend(opts)
assertKeys()

this.name = name
this.value = value || ""
var value = opts.encoded
? this.decode(name + '.b64')
: this.get(name)
if (!value) return

for (var name in attrs) this[name] = attrs[name]
}
var remote = this.decode(name + '.sig', true)
if (!remote) return // no signature

Cookie.prototype = {
path: "/",
expires: undefined,
domain: undefined,
httpOnly: true,
secure: false,
overwrite: false,
var data = name + '=' + value
var index = keys.index(data, remote)

toString: function() {
return this.name + "=" + this.value
},
// invalid signature, so we clear it
if (index < 0) return this.clear(name + '.sig', opts)

// update the signature to the latest key
// to do: update the original cookie as well
if (index > 0) this.encode(name + '.sig', data, opts)
return value
}

Cookies.prototype.sign = function (name, value, opts) {
opts = this.extend(opts)
assertKeys()

if (opts.encoded) this.encode(name + '.b64', value, opts)
else this.set(name, value, opts)
this.encode(name + '.sig', keys.sign(name + '=' + value), opts)
return this
}

toHeader: function() {
var header = this.toString()
Cookies.prototype.decrypt = function (name, opts) {
opts = this.extend(opts)
assertKeys()

if (this.maxage) this.expires = new Date(Date.now() + this.maxage);
var value = this.decode(name + '.enc', true)
if (!value) return

if (this.path ) header += "; path=" + this.path
if (this.expires ) header += "; expires=" + this.expires.toUTCString()
if (this.domain ) header += "; domain=" + this.domain
if (this.secure ) header += "; secure"
if (this.httpOnly ) header += "; httponly"
var msg = keys.decrypt(value)
if (!msg) return // bad decryption

return header
// re-encrypt if not using the latest key
if (msg[1] > 0) this.encrypt(name, msg[0], opts)
return msg[0].toString('utf8')
}
}

function getPattern(name) {
if (cache[name]) return cache[name]
Cookies.prototype.encrypt = function (name, value, opts) {
opts = this.extend(opts)
assertKeys()

return cache[name] = new RegExp(
"(?:^|;) *" +
name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") +
"=([^;]*)"
)
}
this.encode(name + '.enc', keys.encrypt(value), opts)
return this
}

function pushCookie(cookies, cookie) {
if (cookie.overwrite) {
cookies = cookies.filter(function(c) { return c.indexOf(cookie.name+'=') !== 0 })
// clear a cookie
Cookies.prototype.clear = function (name, opts) {
this.set(name, null, opts)
}
cookies.push(cookie.toHeader())
return cookies
}

Cookies.connect = Cookies.express = function(keys) {
return function(req, res, next) {
req.cookies = res.cookies = new Cookies(req, res, keys)
next()
// inherit the options from the main options
Cookies.prototype.extend = function (opts) {
return merge(Object.create(options), opts || {})
}
}

Cookies.Cookie = Cookie
return Cookies

module.exports = Cookies
function assertKeys() {
if (!keys) throw new Error('.keys required for signed cookies')
}
}
26 changes: 26 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

var cache = {}
exports.getPattern = function getPattern(name) {
if (cache[name]) return cache[name]

return cache[name] = new RegExp(
"(?:^|;) *" +
name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") +
"=([^;]*)"
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this actually do?

See: #9

Breif comments on some of these thing would be useful before 1.0.0


exports.pushCookie = function pushCookie(cookies, cookie) {
if (cookie.overwrite) {
cookies = cookies.filter(function(c) {
return c.indexOf(cookie.name+'=') !== 0
})
}
cookies.push(cookie.toHeader())
return cookies
}

exports.merge = function merge(dest, src) {
for (var name in src) dest[name] = src[name]
return dest
}
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
"description": "Cookies, optionally signed using Keygrip.",
"main": "./lib/cookies",
"dependencies": {
"keygrip": "~1.0.0"
"keygrip": "expressjs/keygrip",
"base64-url": "~1.0.0"
},
"devDependencies": {
"express": "*",
"restify": "*",
"supertest": "0",
"mocha": "1"
},
Expand All @@ -19,6 +18,6 @@
"author": "Jed Schmidt <tr@nslator.jp> (http://jed.is)",
"repository": "expressjs/cookies",
"scripts": {
"test": "mocha --reporter spec"
"test": "mocha --reporter spec test/index.js"
}
}
41 changes: 41 additions & 0 deletions test/encoded.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

describe('Base64 Encoded Cookies', function () {
describe('Getting/Setting', function () {
var string = "something \n really \n crazy"
var cookie

before(function () {
serve(function (req, res) {
var cookies = Cookies(req, res)

if (req.method === 'POST') {
cookies.encode('unsigned', string)
} else {
res.end(cookies.decode('unsigned'))
}

res.end()
})
})

it('should set an encoded cookie', function (done) {
request(server)
.post('/')
.expect('Set-Cookie', /^unsigned=[\w-]+; path=\/; httponly$/)
.expect(200, function (err, res) {
if (err) return done(err)

cookie = res.headers['set-cookie'][0]
done()
})
})

it('should get an encoded cookie', function (done) {
request(server)
.get('/')
.set('Cookie', cookie)
.expect(200)
.expect(string, done)
})
})
})
Loading