From c88baf446f1057e72575d47650d78e83520cc414 Mon Sep 17 00:00:00 2001 From: Nathan Woltman Date: Wed, 19 Jun 2019 21:41:22 -0400 Subject: [PATCH] feat: Support cookie.parse()'s `decode` option --- README.md | 67 ++++++++++++++++++++++++++++++++++++++-------------- cookie.js | 22 ++++++++--------- test/test.js | 23 ++++++++++++++++++ 3 files changed, 83 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 78e4351..aef18b2 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,8 @@ ## Installation ```sh -# npm npm install @medley/cookie --save - -# yarn +# or yarn add @medley/cookie ``` @@ -25,22 +23,55 @@ yarn add @medley/cookie const medley = require('@medley/medley'); const app = medley(); +app.register(require('@medley/cookie')); + +app.get('/', (req, res) => { + if (req.cookies.foo === undefined) { + res.setCookie('foo', 'bar'); + res.send('cookie set'); + } else { + res.send(`cookie: foo = ${req.cookies.foo}`); + } +}); +``` + +### Plugin Options + +#### `secret` + +Type: `string` + +Used for signing/unsigning cookies. + +```js app.register(require('@medley/cookie'), { secret: 'to everybody', // `secret` should be a long, random string }); app.get('/', (req, res) => { if (req.cookies.foo === undefined) { - res.setCookie('foo', 'bar'); + const fooCookie = res.signCookie('foo-value') + res.setCookie('foo', foo); res.send('cookie set'); } else { - res.send('foo = ' + req.cookies.foo); - // Sends: 'foo = bar' + const fooCookie = req.unsignCookie(req.cookies.foo); + res.send(`foo = ${fooCookie}`); } }); ``` -The cookie plugin takes one option called `secret` which is used for signing/unsigning cookies. +#### `decode` + +Type: `function`
+Default: [`decodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) + +A function that will be used to decode each cookies' value. + +```js +app.register(require('@medley/cookie'), { + decode: require('safe-decode-uri-component'), +}); +``` ## API @@ -57,12 +88,11 @@ The cookie plugin takes one option called `secret` which is used for signing/uns An object of parsed cookies. ```js -req.cookies // { cookieName: 'cookieValue', foo: 'bar' } +app.get('/', (req, res) => { + req.cookies // { cookieName: 'cookieValue', foo: 'bar' } +}); ``` -Note that cookies are parsed in an `onRequest` hook, so `req.cookies` will not -be available in any `onRequest` hooks added before registering this plugin. - ### `req.unsignCookie(value)` + `value` *(string)* - A cookie value. @@ -88,16 +118,17 @@ Signs a cookie value. ```js const signedValue = res.signCookie('hello'); console.log(signedValue); // 'hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI' + res.setCookie('greeting', signedValue); ``` -The signed value will be different depending on the `secret` option used when +The signed value will be different depending on the [`secret`](#secret) option used when registering the plugin. ### `res.setCookie(name, value[, options])` + `name` *(string)* - The name of the cookie. -+ `value` *(string)* - A cookie value. ++ `value` *(string)* - The cookie value. + `options` *(object)* - See the [options](#options) below. + chainable @@ -128,13 +159,13 @@ res.setCookie('cross_domain_cookie', 'value!', { | Property | Type | Description | |----------|------|-------------| | `domain` | *string* | Domain name for the cookie. -| `encode` | *function* | A synchronous function used for encoding the cookie value. Default: [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). +| `encode` | *function* | A synchronous function used for encoding the cookie value.
Default: [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) | `expires` | *Date* | Expiry date of the cookie in GMT. If not specified (and `maxAge` is not specified), a session cookie is created. -| `httpOnly` | *boolean* | Flags the cookie to be accessible only by the web server (and not by JavaScript in the browser). Default: `false`. +| `httpOnly` | *boolean* | Flags the cookie to be accessible only by the web server (and not by JavaScript in the browser).
Default: `false` | `maxAge` | *number* | Convenient option for setting the expiry time relative to the current time in **seconds**. If not specified (and `expires` is not specified), a session cookie is created. -| `path` | *string* | URL path prefix at which the cookie will be available. Default: `'/'`. -| `sameSite` | *string* | Value of the [*SameSite*](https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1) `Set-Cookie` attribute (either `'strict'` or `'lax'`). -| `secure` | *boolean* | Flags the cookie to be used with HTTPS only. Default: `false`. +| `path` | *string* | URL path prefix at which the cookie will be available.
Default: `'/'` +| `sameSite` | *string*\|*boolean* | Value for the [`SameSite`](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3.7) `Set-Cookie` attribute. Can be any of the values supported by the [`cookie` module](https://github.com/jshttp/cookie#samesite): `true`, `false`, `'strict'`, `'lax'`, `'none'`. +| `secure` | *boolean* | Flags the cookie to be used with HTTPS only.
Default: `false` ### `res.clearCookie(name[, options])` diff --git a/cookie.js b/cookie.js index 7d4d6d2..bd10e6b 100644 --- a/cookie.js +++ b/cookie.js @@ -3,13 +3,18 @@ const {parse, serialize} = require('cookie'); const {sign, unsign} = require('cookie-signature'); -function cookie(app, {secret} = {}) { +function cookie(app, {decode, secret} = {}) { app.decorateRequest('cookies', null); - app.addHook('onRequest', onRequest); + const parseOpts = {decode}; - app.decorateResponse('setCookie', setCookie); - app.decorateResponse('clearCookie', clearCookie); + app.addHook('onRequest', function onRequest(req, res, next) { + const cookieHeader = req.headers.cookie; + + req.cookies = cookieHeader === undefined ? {} : parse(cookieHeader, parseOpts); + + next(); + }); app.decorateRequest('unsignCookie', function unsignCookie(value) { return unsign(value, secret); @@ -18,14 +23,9 @@ function cookie(app, {secret} = {}) { app.decorateResponse('signCookie', function signCookie(value) { return sign(value, secret); }); -} - -function onRequest(req, res, next) { - const cookieHeader = req.headers.cookie; - - req.cookies = cookieHeader === undefined ? {} : parse(cookieHeader); - next(); + app.decorateResponse('setCookie', setCookie); + app.decorateResponse('clearCookie', clearCookie); } function setCookie(name, value, options) { diff --git a/test/test.js b/test/test.js index ef2d3a1..d625a0e 100644 --- a/test/test.js +++ b/test/test.js @@ -37,6 +37,29 @@ describe('req.cookies', () => { assert.strictEqual(res.body, 'success'); }); + it('should support parsed cookies using the decode option', async () => { + const app = makeApp({ + decode: str => str.replace(/-/g, '_'), + }); + + app.get('/', (req, res) => { + assert.deepStrictEqual(req.cookies, { + foo: 'bar_buzz', + zab: '_a%20buzz_buzz', + }); + + res.send('success'); + }); + + const res = await app.request({ + url: '/', + headers: { + Cookie: 'foo=bar-buzz; zab=-a%20buzz-buzz', + }, + }); + assert.strictEqual(res.body, 'success'); + }); + });