diff --git a/.eslintignore b/.eslintignore index 62562b7..3e841d3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ coverage node_modules +/test/support/supertest/http2wrapper.js \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index c98f746..c914a9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ node_js: - "7.10" - "8.9" - "9.3" + - "10.10" sudo: false cache: directories: @@ -34,3 +35,7 @@ script: - "test -z $(npm -ps ls eslint ) || npm run-script lint" after_script: - "test -e ./coverage/lcov.info && npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" +matrix: + include: + - node_js: "10" + env: HTTP2_TEST=1 diff --git a/index.js b/index.js index 480fd19..f6fc7aa 100644 --- a/index.js +++ b/index.js @@ -74,7 +74,7 @@ function vhost (hostname, handle) { */ function hostnameof (req) { - var host = req.headers.host + var host = ishttp2(req) ? req.headers[':authority'] : req.headers.host if (!host) { return @@ -137,7 +137,7 @@ function hostregexp (val) { */ function vhostof (req, regexp) { - var host = req.headers.host + var host = ishttp2(req) ? req.headers[':authority'] : req.headers.host var hostname = hostnameof(req) if (!hostname) { @@ -162,3 +162,15 @@ function vhostof (req, regexp) { return obj } + +/** + * Check if a request is a http2 request. + * + * @param {Object} request + * @return {Boolean} + * @public + */ + +function ishttp2 (req) { + return req.httpVersionMajor === 2 +} diff --git a/package.json b/package.json index a137963..0dcd333 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "eslint-plugin-standard": "3.0.1", "istanbul": "0.4.5", "mocha": "2.5.3", - "supertest": "1.1.0" + "supertest": "2.0" }, "files": [ "LICENSE", @@ -31,6 +31,7 @@ "scripts": { "lint": "eslint --plugin markdown --ext js,md .", "test": "mocha --reporter spec --bail --check-leaks test/", + "test-http2": "HTTP2_TEST=1 mocha --reporter spec --bail --check-leaks test/", "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" } diff --git a/test/support/supertest/http2wrapper.js b/test/support/supertest/http2wrapper.js new file mode 100644 index 0000000..4e87068 --- /dev/null +++ b/test/support/supertest/http2wrapper.js @@ -0,0 +1,188 @@ +'use strict'; + +const http2 = require('http2'); +const Stream = require('stream'); +const util = require('util'); +const net = require('net'); +const tls = require('tls'); +const parse = require('url').parse; + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_HOST, + HTTP2_HEADER_SET_COOKIE, + NGHTTP2_CANCEL, +} = http2.constants; + + +function setProtocol(protocol) { + return { + request: function (options) { + return new Request(protocol, options); + } + } +} + +function Request(protocol, options) { + Stream.call(this); + const defaultPort = protocol === 'https:' ? 443 : 80; + const defaultHost = 'localhost' + const port = options.port || defaultPort; + const host = options.host || defaultHost; + + delete options.port + delete options.host + + this.method = options.method.toUpperCase(); + this.path = options.path; + this.protocol = protocol; + this.host = host; + + delete options.method + delete options.path + + const sessionOptions = Object.assign({}, options); + if (options.socketPath) { + sessionOptions.socketPath = options.socketPath; + sessionOptions.createConnection = this.createUnixConnection.bind(this); + } + + this._headers = {}; + + const session = http2.connect(`${protocol}//${host}:${port}`, sessionOptions); + this.setHeader('host', `${host}:${port}`) + + session.on('error', (err) => this.emit('error', err)); + + this.session = session; +} + +/** + * Inherit from `Stream` (which inherits from `EventEmitter`). + */ +util.inherits(Request, Stream); + +Request.prototype.createUnixConnection = function (authority, options) { + switch (this.protocol) { + case 'http:': + return net.connect(options.socketPath); + case 'https:': + options.ALPNProtocols = ['h2']; + options.servername = this.host; + options.allowHalfOpen = true; + return tls.connect(options.socketPath, options); + default: + throw new Error('Unsupported protocol', this.protocol); + } +} + +Request.prototype.setNoDelay = function (bool) { + // We can not use setNoDelay with HTTP/2. + // Node 10 limits http2session.socket methods to ones safe to use with HTTP/2. + // See also https://nodejs.org/api/http2.html#http2_http2session_socket +} + +Request.prototype.getFrame = function () { + if (this.frame) { + return this.frame; + } + + const method = { + [HTTP2_HEADER_PATH]: this.path, + [HTTP2_HEADER_METHOD]: this.method, + } + + let headers = this.mapToHttp2Header(this._headers); + + headers = Object.assign(headers, method); + + const frame = this.session.request(headers); + frame.once('response', (headers, flags) => { + headers = this.mapToHttpHeader(headers); + frame.headers = headers; + frame.status = frame.statusCode = headers[HTTP2_HEADER_STATUS]; + this.emit('response', frame); + }); + + this._headerSent = true; + + frame.once('drain', () => this.emit('drain')); + frame.on('error', (err) => this.emit('error', err)); + frame.on('close', () => this.session.close()); + + this.frame = frame; + return frame; +} + +Request.prototype.mapToHttpHeader = function (headers) { + const keys = Object.keys(headers); + const http2Headers = {}; + for (var i = 0; i < keys.length; i++) { + let key = keys[i]; + let value = headers[key]; + key = key.toLowerCase(); + switch (key) { + case HTTP2_HEADER_SET_COOKIE: + value = Array.isArray(value) ? value : [value]; + break; + default: + break; + } + http2Headers[key] = value; + } + return http2Headers; +} + +Request.prototype.mapToHttp2Header = function (headers) { + const keys = Object.keys(headers); + const http2Headers = {}; + for (var i = 0; i < keys.length; i++) { + let key = keys[i]; + let value = headers[key]; + key = key.toLowerCase(); + switch (key) { + case HTTP2_HEADER_HOST: + key = HTTP2_HEADER_AUTHORITY; + value = /^http\:\/\/|^https\:\/\//.test(value) ? parse(value).host : value; + break; + default: + break; + } + http2Headers[key] = value; + } + return http2Headers; +} + +Request.prototype.setHeader = function (name, value) { + this._headers[name.toLowerCase()] = value; +} + +Request.prototype.getHeader = function (name) { + return this._headers[name.toLowerCase()]; +} + +Request.prototype.write = function (data, encoding) { + const frame = this.getFrame(); + return frame.write(data, encoding); +}; + +Request.prototype.pipe = function (stream, options) { + const frame = this.getFrame(); + return frame.pipe(stream, options); +} + +Request.prototype.end = function (data) { + const frame = this.getFrame(); + frame.end(data); +} + +Request.prototype.abort = function (data) { + const frame = this.getFrame(); + frame.close(NGHTTP2_CANCEL); + this.session.destroy(); +} + +exports.setProtocol = setProtocol; diff --git a/test/support/supertest/index.js b/test/support/supertest/index.js new file mode 100644 index 0000000..cf424dc --- /dev/null +++ b/test/support/supertest/index.js @@ -0,0 +1,32 @@ +var request = require('supertest') + +if (process.env.HTTP2_TEST) { + var http2 = require('http2') + var http2wrapper = require('./http2wrapper') + var agent = require('superagent') + var tls = require('tls') + agent.protocols = { + 'http:': http2wrapper.setProtocol('http:'), + 'https:': http2wrapper.setProtocol('https:') + } + request.Test.prototype.serverAddress = function (app, path, host) { + var addr = app.address() + var port + var protocol + + if (!addr) this._server = app.listen(0) + port = app.address().port + + protocol = app instanceof tls.Server ? 'https' : 'http' + return protocol + '://' + (host || '127.0.0.1') + ':' + port + path + } + var originalRequest = request + request = function (app) { + if (typeof app === 'function') { + app = http2.createServer(app) + } + return originalRequest(app) + } +} + +module.exports = request diff --git a/test/test.js b/test/test.js index 7d377b2..a45b67c 100644 --- a/test/test.js +++ b/test/test.js @@ -1,9 +1,13 @@ var assert = require('assert') var http = require('http') -var request = require('supertest') +var request = require('./support/supertest') var vhost = require('..') +if (process.env.HTTP2_TEST) { + http = require('http2') +} + describe('vhost(hostname, server)', function () { it('should route by Host', function (done) { var vhosts = []