diff --git a/lib/application.js b/lib/application.js index 9aec792..84107cb 100644 --- a/lib/application.js +++ b/lib/application.js @@ -14,7 +14,6 @@ const isJSON = require('koa-is-json'); const context = require('./context'); const request = require('./request'); const statuses = require('statuses'); -const Cookies = require('cookies'); const Emitter = require('events'); const util = require('util'); const Stream = require('stream'); @@ -169,11 +168,6 @@ module.exports = class Application extends Emitter { request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; - context.cookies = new Cookies(req, res, { - keys: this.keys, - secure: request.secure - }); - request.ip = request.ips[0] || req.socket.remoteAddress || ''; context.state = {}; return context; } diff --git a/lib/context.js b/lib/context.js index 88ec1ae..1b92f01 100644 --- a/lib/context.js +++ b/lib/context.js @@ -10,6 +10,9 @@ const createError = require('http-errors'); const httpAssert = require('http-assert'); const delegate = require('delegates'); const statuses = require('statuses'); +const Cookies = require('cookies'); + +const COOKIES = Symbol('context#cookies'); /** * Context prototype. @@ -151,6 +154,20 @@ const proto = module.exports = { this.status = err.status; this.length = Buffer.byteLength(msg); this.res.end(msg); + }, + + get cookies() { + if (!this[COOKIES]) { + this[COOKIES] = new Cookies(this.req, this.res, { + keys: this.app.keys, + secure: this.request.secure + }); + } + return this[COOKIES]; + }, + + set cookies(_cookies) { + this[COOKIES] = _cookies; } }; diff --git a/lib/request.js b/lib/request.js index 9ef7c49..9f56410 100644 --- a/lib/request.js +++ b/lib/request.js @@ -17,6 +17,8 @@ const fresh = require('fresh'); const only = require('only'); const util = require('util'); +const IP = Symbol('context#ip'); + /** * Prototype. */ @@ -439,6 +441,26 @@ module.exports = { : []; }, + /** + * Return request's remote address + * When `app.proxy` is `true`, parse + * the "X-Forwarded-For" ip address list and return the first one + * + * @return {String} + * @api public + */ + + get ip() { + if (!this[IP]) { + this[IP] = this.ips[0] || this.socket.remoteAddress || ''; + } + return this[IP]; + }, + + set ip(_ip) { + this[IP] = _ip; + }, + /** * Return subdomains as an array. * diff --git a/test/context/cookies.js b/test/context/cookies.js index c6daf53..e12df80 100644 --- a/test/context/cookies.js +++ b/test/context/cookies.js @@ -5,51 +5,13 @@ const assert = require('assert'); const request = require('supertest'); const Koa = require('../..'); -describe('ctx.cookies.set()', () => { - it('should set an unsigned cookie', async () => { - const app = new Koa(); - - app.use((ctx, next) => { - ctx.cookies.set('name', 'jon'); - ctx.status = 204; - }); - - const server = app.listen(); - - const res = await request(server) - .get('/') - .expect(204); - - const cookie = res.headers['set-cookie'].some(cookie => /^name=/.test(cookie)); - assert.equal(cookie, true); - }); - - describe('with .signed', () => { - describe('when no .keys are set', () => { - it('should error', () => { - const app = new Koa(); - - app.use((ctx, next) => { - try { - ctx.cookies.set('foo', 'bar', { signed: true }); - } catch (err) { - ctx.body = err.message; - } - }); - - return request(app.callback()) - .get('/') - .expect('.keys required for signed cookies'); - }); - }); - - it('should send a signed cookie', async () => { +describe('ctx.cookies', () => { + describe('ctx.cookies.set()', () => { + it('should set an unsigned cookie', async () => { const app = new Koa(); - app.keys = ['a', 'b']; - app.use((ctx, next) => { - ctx.cookies.set('name', 'jon', { signed: true }); + ctx.cookies.set('name', 'jon'); ctx.status = 204; }); @@ -59,36 +21,99 @@ describe('ctx.cookies.set()', () => { .get('/') .expect(204); - const cookies = res.headers['set-cookie']; + const cookie = res.headers['set-cookie'].some(cookie => /^name=/.test(cookie)); + assert.equal(cookie, true); + }); - assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true); - assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true); + describe('with .signed', () => { + describe('when no .keys are set', () => { + it('should error', () => { + const app = new Koa(); + + app.use((ctx, next) => { + try { + ctx.cookies.set('foo', 'bar', { signed: true }); + } catch (err) { + ctx.body = err.message; + } + }); + + return request(app.callback()) + .get('/') + .expect('.keys required for signed cookies'); + }); + }); + + it('should send a signed cookie', async () => { + const app = new Koa(); + + app.keys = ['a', 'b']; + + app.use((ctx, next) => { + ctx.cookies.set('name', 'jon', { signed: true }); + ctx.status = 204; + }); + + const server = app.listen(); + + const res = await request(server) + .get('/') + .expect(204); + + const cookies = res.headers['set-cookie']; + + assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true); + assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true); + }); + }); + + describe('with secure', () => { + it('should get secure from request', async () => { + const app = new Koa(); + + app.proxy = true; + app.keys = ['a', 'b']; + + app.use(ctx => { + ctx.cookies.set('name', 'jon', { signed: true }); + ctx.status = 204; + }); + + const server = app.listen(); + + const res = await request(server) + .get('/') + .set('x-forwarded-proto', 'https') // mock secure + .expect(204); + + const cookies = res.headers['set-cookie']; + assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true); + assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true); + assert.equal(cookies.every(cookie => /secure/.test(cookie)), true); + }); }); }); - describe('with secure', () => { - it('should get secure from request', async () => { + describe('ctx.cookies=', () => { + it('should override cookie work', async () => { const app = new Koa(); - app.proxy = true; - app.keys = ['a', 'b']; - - app.use(ctx => { - ctx.cookies.set('name', 'jon', { signed: true }); + app.use((ctx, next) => { + ctx.cookies = { + set(key, value){ + ctx.set(key, value); + } + }; + ctx.cookies.set('name', 'jon'); ctx.status = 204; }); const server = app.listen(); - const res = await request(server) + await request(server) .get('/') - .set('x-forwarded-proto', 'https') // mock secure + .expect('name', 'jon') .expect(204); - - const cookies = res.headers['set-cookie']; - assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true); - assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true); - assert.equal(cookies.every(cookie => /secure/.test(cookie)), true); }); }); }); diff --git a/test/request/ip.js b/test/request/ip.js index 8c8cb09..3375f70 100644 --- a/test/request/ip.js +++ b/test/request/ip.js @@ -39,11 +39,21 @@ describe('req.ip', () => { }); }); - it('should be cached', () => { + it('should be lazy inited and cached', () => { const req = { socket: new Stream.Duplex() }; req.socket.remoteAddress = '127.0.0.2'; const request = Request(req); + assert.equal(request.ip, '127.0.0.2'); req.socket.remoteAddress = '127.0.0.1'; assert.equal(request.ip, '127.0.0.2'); }); + + it('should reset ip work', () => { + const req = { socket: new Stream.Duplex() }; + req.socket.remoteAddress = '127.0.0.2'; + const request = Request(req); + assert.equal(request.ip, '127.0.0.2'); + request.ip = '127.0.0.1'; + assert.equal(request.ip, '127.0.0.1'); + }); });