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

Release 0.6 #68

Merged
merged 8 commits into from
Mar 1, 2016
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
2 changes: 0 additions & 2 deletions .npmignore

This file was deleted.

10 changes: 10 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
0.6.x
=====

* Add `secure` constructor option for secure connection checking
* Change constructor to signature `new Cookies(req, res, [options])`
- Replace `new Cookies(req, res, key)` with `new Cookies(req, res, {'keys': keys})`
* Change prototype construction for proper "constructor" property
* Deprecate `secureProxy` option in `.set`; use `secure` option instead
- If `secure: true` throws even over SSL, use the `secure` constructor option

0.5.1 / 2014-07-27
==================

Expand Down
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
Cookies
=======

[![NPM Version](https://badge.fury.io/js/cookies.svg)](https://badge.fury.io/js/cookies)
[![Build Status](https://travis-ci.org/pillarjs/cookies.svg?branch=master)](https://travis-ci.org/pillarjs/cookies)
[![NPM Version][npm-image]][npm-url]
[![NPM Downloads][downloads-image]][downloads-url]
[![Node.js Version][node-version-image]][node-version-url]
[![Build Status][travis-image]][travis-url]

Cookies is a [node.js](http://nodejs.org/) module for getting and setting HTTP(S) cookies. Cookies can be signed to prevent tampering, using [Keygrip](https://www.npmjs.com/package/keygrip). It can be used with the built-in node.js HTTP library, or as Connect/Express middleware.

Expand All @@ -26,9 +28,13 @@ Cookies is a [node.js](http://nodejs.org/) module for getting and setting HTTP(S

## API

### cookies = new Cookies( request, response, [ keys ] )
### cookies = new Cookies( request, response, [ options ] )

This creates a cookie jar corresponding to the current _request_ and _response_. A [Keygrip](https://www.npmjs.com/package/keygrip) object or an array of keys can optionally be passed as the third argument _keygrip_ to enable cryptographic signing based on SHA1 HMAC, using rotated credentials.
This creates a cookie jar corresponding to the current _request_ and _response_, additionally passing an object _options_.

A [Keygrip](https://www.npmjs.com/package/keygrip) object or an array of keys can optionally be passed as _options.keys_ to enable cryptographic signing based on SHA1 HMAC, using rotated credentials.

A Boolean can optionally be passed as _options.secure_ to explicitally specify if the connection is secure, rather than this module examining _request_.

Note that since this only saves parameters without any other processing, it is very lightweight. Cookies are only parsed on demand when they are accessed.

Expand Down Expand Up @@ -61,7 +67,6 @@ If the _options_ object is provided, it will be used to generate the outbound co
* `path`: a string indicating the path of the cookie (`/` by default).
* `domain`: a string indicating the domain of the cookie (no default).
* `secure`: a boolean indicating whether the cookie is only to be sent over HTTPS (`false` by default for HTTP, `true` by default for HTTPS).
* `secureProxy`: a boolean indicating whether the cookie is only to be sent over HTTPS (use this if you handle SSL not in your node process).
* `httpOnly`: a boolean indicating whether the cookie is only to be sent over HTTP(S), and not made available to client JavaScript (`true` by default).
* `signed`: a boolean indicating whether the cookie is to be signed (`false` by default). If this is true, another cookie of the same name with the `.sig` suffix appended will also be sent, with a 27-byte url-safe base64 SHA1 value representing the hash of _cookie-name_=_cookie-value_ against the first [Keygrip](https://www.npmjs.com/package/keygrip) key. This signature key is used to detect tampering the next time a cookie is received.
* `overwrite`: a boolean indicating whether to overwrite previously set cookies of the same name (`false` by default). If this is true, all cookies set during the same request with the same name (regardless of path or domain) are filtered out of the Set-Cookie header when setting this cookie.
Expand All @@ -73,7 +78,7 @@ var http = require( "http" )
var Cookies = require( "cookies" )

server = http.createServer( function( req, res ) {
var cookies = new Cookies( req, res, keys )
var cookies = new Cookies( req, res, { "keys": keys } )
, unsigned, signed, tampered

if ( req.url == "/set" ) {
Expand Down Expand Up @@ -119,3 +124,12 @@ Copyright
Copyright (c) 2014 Jed Schmidt. See LICENSE.txt for details.

Send any questions or comments [here](http://twitter.com/jedschmidt).

[npm-image]: https://img.shields.io/npm/v/cookies.svg
[npm-url]: https://npmjs.org/package/cookies
[downloads-image]: https://img.shields.io/npm/dm/cookies.svg
[downloads-url]: https://npmjs.org/package/cookies
[node-version-image]: https://img.shields.io/node/v/cookies.svg
[node-version-url]: https://nodejs.org/en/download/
[travis-image]: https://img.shields.io/travis/pillarjs/cookies/master.svg
[travis-url]: https://travis-ci.org/pillarjs/cookies
171 changes: 92 additions & 79 deletions lib/cookies.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
var deprecate = require('depd')('cookies')
var Keygrip = require('keygrip')
var http = require('http')
var cache = {}
Expand All @@ -12,82 +13,93 @@ var cache = {}

var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;

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

this.secure = undefined
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

if (options) {
if (Array.isArray(options)) {
// array of key strings
deprecate('"keys" argument; provide using options {"key": [...]}')
this.keys = new Keygrip(options)
} else if (options.constructor && options.constructor.name === 'Keygrip') {
// any keygrip constructor to allow different versions
deprecate('"keys" argument; provide using options {"key": keygrip}')
this.keys = options
} else {
this.keys = options.keys
this.secure = options.secure
}
}
}

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

header = this.request.headers["cookie"]
if (!header) return
header = this.request.headers["cookie"]
if (!header) return

match = header.match(getPattern(name))
if (!match) return
match = header.match(getPattern(name))
if (!match) return

value = match[1]
if (!opts || !signed) return value
value = match[1]
if (!opts || !signed) return value

remote = this.get(sigName)
if (!remote) return
remote = this.get(sigName)
if (!remote) return

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

if (index < 0) {
this.set(sigName, null, {path: "/", signed: false })
} else {
index && this.set(sigName, this.keys.sign(data), { signed: false })
return value
}
},
if (index < 0) {
this.set(sigName, null, {path: "/", signed: false })
} else {
index && this.set(sigName, this.keys.sign(data), { signed: false })
return value
}
};

set: function(name, value, opts) {
var res = this.response
, req = this.request
, headers = res.getHeader("Set-Cookie") || []
, secure = req.protocol === 'https' || 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) {
var res = this.response
, req = this.request
, headers = res.getHeader("Set-Cookie") || []
, secure = this.secure !== undefined ? !!this.secure : req.protocol === 'https' || req.connection.encrypted
, cookie = new Cookie(name, value, opts)
, signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys

if (typeof headers == "string") headers = [headers]
if (typeof headers == "string") headers = [headers]

if (!secure && opts && opts.secure) {
throw new Error('Cannot send secure cookie over unencrypted connection')
}
if (!secure && opts && opts.secure) {
throw new Error('Cannot send secure cookie over unencrypted connection')
}

cookie.secure = secure
if (opts && "secure" in opts) cookie.secure = opts.secure
if (opts && "secureProxy" in opts) cookie.secure = opts.secureProxy
headers = pushCookie(headers, cookie)
cookie.secure = secure
if (opts && "secure" in opts) cookie.secure = opts.secure

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)
}
if (opts && "secureProxy" in opts) {
deprecate('"secureProxy" option; use "secure" option, provide "secure" to constructor if needed')
cookie.secure = opts.secureProxy
}

headers = pushCookie(headers, cookie)

var setHeader = res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader
setHeader.call(res, 'Set-Cookie', headers)
return this
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)
}
}

var setHeader = res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader
setHeader.call(res, 'Set-Cookie', headers)
return this
};

function Cookie(name, value, attrs) {
if (!fieldContentRegExp.test(name)) {
Expand Down Expand Up @@ -116,32 +128,30 @@ function Cookie(name, value, attrs) {
}
}

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

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

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

if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge);
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"
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
}
}
return header
};

// back-compat so maxage mirrors maxAge
Object.defineProperty(Cookie.prototype, 'maxage', {
Expand Down Expand Up @@ -171,7 +181,10 @@ function pushCookie(cookies, cookie) {

Cookies.connect = Cookies.express = function(keys) {
return function(req, res, next) {
req.cookies = res.cookies = new Cookies(req, res, keys)
req.cookies = res.cookies = new Cookies(req, res, {
keys: keys
})

next()
}
}
Expand Down
15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "Cookies, optionally signed using Keygrip.",
"main": "./lib/cookies",
"dependencies": {
"depd": "~1.1.0",
"keygrip": "~1.0.0"
},
"devDependencies": {
Expand All @@ -12,13 +13,19 @@
"restify": "2.8.1",
"supertest": "1.1.0"
},
"engines": {
"node": ">= 0.8.0"
},
"license": "MIT",
"author": "Jed Schmidt <[email protected]> (http://jed.is)",
"repository": "pillarjs/cookies",
"files": [
"lib/",
"History.md",
"LICENSE.txt",
"README.md"
],
"engines": {
"node": ">= 0.8"
},
"scripts": {
"test": "mocha --reporter spec"
"test": "mocha --require test/support/env --reporter spec"
}
}
5 changes: 5 additions & 0 deletions test/cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ var assert = require('assert')
var cookies = require('..')

describe('new Cookie', function () {
it('should have correct constructor', function () {
var cookie = new cookies.Cookie('foo', 'bar')
assert.equal(cookie.constructor, cookies.Cookie)
})

it('should throw on invalid name', function () {
assert.throws(function () {
new cookies.Cookie('foo\n', 'bar')
Expand Down
44 changes: 44 additions & 0 deletions test/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ describe('HTTP', function () {
var cookies = new Cookies( req, res, keys )
, unsigned, signed, tampered, overwrite

assert.equal( cookies.constructor, Cookies )

if ( req.url == "/set" ) {
cookies
// set a regular cookie
Expand Down Expand Up @@ -80,4 +82,46 @@ describe('HTTP', function () {
.set('Cookie', header.join(';'))
.expect(200, done)
})

describe('with "secure" option', function () {
it('should check connection when undefined; unencrypted', function (done) {
request(createServer( "http", { "keys": keys } ))
.get('/')
.expect(500, 'Cannot send secure cookie over unencrypted connection', done)
})

it('should check connection when undefined; encrypted', function (done) {
request(createServer( "https", { "keys": keys } ))
.get('/')
.expect(200, done)
})

it('should not check connection when defined; true', function (done) {
request(createServer( "http", { "keys": keys, "secure": true } ))
.get('/')
.expect(200, done)
})

it('should not check connection when defined; false', function (done) {
request(createServer( "https", { "keys": keys, "secure": false } ))
.get('/')
.expect(500, 'Cannot send secure cookie over unencrypted connection', done)
})
})
})

function createServer(proto, opts) {
return http.createServer(function (req, res) {
var cookies = new Cookies( req, res, opts )
req.protocol = proto

try {
cookies.set( "foo", "bar", { "secure": true } )
} catch (e) {
res.statusCode = 500
res.write(e.message)
}

res.end()
})
}
2 changes: 2 additions & 0 deletions test/support/env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
process.env.NODE_ENV = 'test'
process.env.NO_DEPRECATION = 'cookies'