diff --git a/docs/api/request.md b/docs/api/request.md index 9b49447..4738fbe 100644 --- a/docs/api/request.md +++ b/docs/api/request.md @@ -98,6 +98,14 @@ ctx.request.href Get hostname when present. Supports `X-Forwarded-Host` when `app.proxy` is __true__, otherwise `Host` is used. + + If host is IPv6, Koa delegates parsing to + [WHATWG URL API](https://nodejs.org/dist/latest-v8.x/docs/api/url.html#url_the_whatwg_url_api), + *Note* This may impact performance. + +### request.URL + + Get WHATWG parsed URL object. ### request.type diff --git a/lib/context.js b/lib/context.js index ece4718..0cbf207 100644 --- a/lib/context.js +++ b/lib/context.js @@ -198,6 +198,7 @@ delegate(proto, 'request') .getter('protocol') .getter('host') .getter('hostname') + .getter('URL') .getter('header') .getter('headers') .getter('secure') diff --git a/lib/request.js b/lib/request.js index 1e31754..23ed3f2 100644 --- a/lib/request.js +++ b/lib/request.js @@ -5,6 +5,7 @@ * Module dependencies. */ +const URL = require('url').URL; const net = require('net'); const contentType = require('content-type'); const stringify = require('url').format; @@ -264,9 +265,32 @@ module.exports = { get hostname() { const host = this.host; if (!host) return ''; + if ('[' == host[0]) return this.URL.hostname || ''; // IPv6 return host.split(':')[0]; }, + /** + * Get WHATWG parsed URL. + * Lazily memoized. + * + * @return {URL|Object} + * @api public + */ + + get URL() { + if (!this.memoizedURL) { + const protocol = this.protocol; + const host = this.host; + const originalUrl = this.originalUrl || ''; // avoid undefined in template string + try { + this.memoizedURL = new URL(`${protocol}://${host}${originalUrl}`); + } catch (err) { + this.memoizedURL = Object.create(null); + } + } + return this.memoizedURL; + }, + /** * Check if the request is fresh, aka * Last-Modified and/or the ETag diff --git a/test/request/hostname.js b/test/request/hostname.js index 14adf40..13134d0 100644 --- a/test/request/hostname.js +++ b/test/request/hostname.js @@ -18,6 +18,38 @@ describe('req.hostname', () => { }); }); + describe('with IPv6 in host', () => { + it('should parse localhost void of port', () => { + const req = request(); + req.header.host = '[::1]'; + assert.equal(req.hostname, '[::1]'); + }); + + it('should parse localhost with port 80', () => { + const req = request(); + req.header.host = '[::1]:80'; + assert.equal(req.hostname, '[::1]'); + }); + + it('should parse localhost with non special schema port', () => { + const req = request(); + req.header.host = '[::1]:1337'; + assert.equal(req.hostname, '[::1]'); + }); + + it('should reduce IPv6 with non special schema port, as hostname', () => { + const req = request(); + req.header.host = '[2001:cdba:0000:0000:0000:0000:3257:9652]:1337'; + assert.equal(req.hostname, '[2001:cdba::3257:9652]'); + }); + + it('should return empty string when invalid', () => { + const req = request(); + req.header.host = '[invalidIPv6]'; + assert.equal(req.hostname, ''); + }); + }); + describe('when X-Forwarded-Host is present', () => { describe('and proxy is not trusted', () => { it('should be ignored', () => { diff --git a/test/request/whatwg-url.js b/test/request/whatwg-url.js new file mode 100644 index 0000000..af13e4c --- /dev/null +++ b/test/request/whatwg-url.js @@ -0,0 +1,26 @@ + +'use strict'; + +const request = require('../helpers/context').request; +const assert = require('assert'); + +describe('req.URL', () => { + describe('should not throw when', () => { + it('host is void', () => { + const req = request(); + assert.doesNotThrow(() => req.URL, TypeError); + }); + + it('header.host is invalid', () => { + const req = request(); + req.header.host = 'invalid host'; + assert.doesNotThrow(() => req.URL, TypeError); + }); + }); + + it('should return empty object when invalid', () => { + const req = request(); + req.header.host = 'invalid host'; + assert.deepStrictEqual(req.URL, Object.create(null)); + }); +});