Use node 7+ WHATWG parser for hostname, fixes #1002 (#1004)

* Use node 7+ WHATWG parser for hostname, fixes #1002

* only use URL if host is IPv6, expose parsed URL

* catch invalid URLs, memoize empty obj

* hostname returns empty string when URL throws
This commit is contained in:
Martin Iwanowski 2017-06-20 18:57:30 +02:00 committed by jongleberry
parent 012587889d
commit 327b65cb6b
5 changed files with 91 additions and 0 deletions

View file

@ -99,6 +99,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
Get request `Content-Type` void of parameters such as "charset".

View file

@ -198,6 +198,7 @@ delegate(proto, 'request')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')

View file

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

View file

@ -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', () => {

View file

@ -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));
});
});