From 61204f9d072fbbbaea1ed92e8d789cfc7fcdc978 Mon Sep 17 00:00:00 2001 From: pini Date: Wed, 9 Aug 2017 16:59:02 +0300 Subject: [PATCH 1/6] Initial support for http2 --- examples/http2/index.js | 34 + examples/http2/keys/test_cert.pem | 16 + examples/http2/keys/test_key.pem | 15 + examples/http2/static/index.html | 16 + examples/http2/static/s1.js | 1 + examples/http2/static/s2.js | 1 + examples/http2/static/s3.js | 1 + examples/http2/static/style.css | 8 + lib/application.js | 24 +- lib/express.js | 19 +- lib/http2Request.js | 23 + lib/http2Response.js | 29 + lib/request.js | 488 +-------------- lib/requestDecorator.js | 518 ++++++++++++++++ lib/response.js | 977 +---------------------------- lib/responseDecorator.js | 992 ++++++++++++++++++++++++++++++ lib/utils.js | 14 + 17 files changed, 1711 insertions(+), 1465 deletions(-) create mode 100644 examples/http2/index.js create mode 100644 examples/http2/keys/test_cert.pem create mode 100644 examples/http2/keys/test_key.pem create mode 100644 examples/http2/static/index.html create mode 100644 examples/http2/static/s1.js create mode 100644 examples/http2/static/s2.js create mode 100644 examples/http2/static/s3.js create mode 100644 examples/http2/static/style.css create mode 100644 lib/http2Request.js create mode 100644 lib/http2Response.js create mode 100644 lib/requestDecorator.js create mode 100644 lib/responseDecorator.js diff --git a/examples/http2/index.js b/examples/http2/index.js new file mode 100644 index 0000000000..842996a7ac --- /dev/null +++ b/examples/http2/index.js @@ -0,0 +1,34 @@ +var http2 = require('http2'); +var fs = require('fs'); +var path = require('path'); +var express = require('../../'); +var keys = path.join(__dirname, 'keys'); +var app = express(); +var style = fs.readFileSync(path.resolve(__dirname, './static/style.css'), 'utf8'); +function pushStyle(res) { + res.createPushResponse({ + ':path': '/style.css', + ':status': 200, + 'content-type': 'text/css' + }, function(err, newResponse) { + newResponse.setHeader('content-type', 'text/css'); + newResponse.end(style); + }); +} + +app.use(express.static('static', {setHeaders: function(res, file) { + if (file.indexOf('index.html') > -1) { + pushStyle(res); + } +}})); + +var server = http2.createSecureServer({ + key: fs.readFileSync(path.join(keys, 'test_key.pem')), + cert: fs.readFileSync(path.join(keys, 'test_cert.pem')) +}, app); + +/* istanbul ignore next */ +if (!module.parent) { + server.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/examples/http2/keys/test_cert.pem b/examples/http2/keys/test_cert.pem new file mode 100644 index 0000000000..6ff9b127cf --- /dev/null +++ b/examples/http2/keys/test_cert.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICjzCCAfgCCQCduAjYszOZ3DANBgkqhkiG9w0BAQUFADCBizELMAkGA1UEBhMC +Q04xEzARBgNVBAgTCkd1YW5nIERvbmcxFDASBgNVBAcTC0d1YW5nIFpob3VlMQ4w +DAYDVQQKEwVUQkVEUDENMAsGA1UECxMEVEVTVDEQMA4GA1UEAxMHZmVuZ21rMjEg +MB4GCSqGSIb3DQEJARYRZmVuZ21rMkBnbWFpbC5jb20wHhcNMTIwOTIzMTQxMDI5 +WhcNMTIxMDIzMTQxMDI5WjCBizELMAkGA1UEBhMCQ04xEzARBgNVBAgTCkd1YW5n +IERvbmcxFDASBgNVBAcTC0d1YW5nIFpob3VlMQ4wDAYDVQQKEwVUQkVEUDENMAsG +A1UECxMEVEVTVDEQMA4GA1UEAxMHZmVuZ21rMjEgMB4GCSqGSIb3DQEJARYRZmVu +Z21rMkBnbWFpbC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALOEtchk +KBK8WTqwXR2Aov2mc0+igyQTGbxBDSyyULHPiecMqOBHs5bV4DL1pc/01hLKIp4T +2j2KNTTmeivrtKd3wMQL7A+IgyqdmeqRi98pYUylFZrHxb9Kiwm7mpHanodmgnTT +zOluEpi/K9h9zM0DbIOynsOh9/w4E2Aq6JvrAgMBAAEwDQYJKoZIhvcNAQEFBQAD +gYEAnPd0JvCKQQBrm9jI6TkJKmfBa4NH0wUpMQv/bo2NWw1tA8fTQYb0S4aTep5Q +JdYctLQeE7abY1fpXFIwFY/FC0rE3alkEK+4PlCXvHGTYMiq90oH0JtlEqYTdTWJ +i99gtHarMEfzejyY3VDa2XFGmZrQCP6Co5NGDjAEr2A4ECg= +-----END CERTIFICATE----- diff --git a/examples/http2/keys/test_key.pem b/examples/http2/keys/test_key.pem new file mode 100644 index 0000000000..dc4cb7297d --- /dev/null +++ b/examples/http2/keys/test_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQCzhLXIZCgSvFk6sF0dgKL9pnNPooMkExm8QQ0sslCxz4nnDKjg +R7OW1eAy9aXP9NYSyiKeE9o9ijU05nor67Snd8DEC+wPiIMqnZnqkYvfKWFMpRWa +x8W/SosJu5qR2p6HZoJ008zpbhKYvyvYfczNA2yDsp7Doff8OBNgKuib6wIDAQAB +AoGAAp2tdHUZLGS4PCWzxalJNr8FMSTiGlV464hbI8qZaG3oyYgisdn5oPoO4U85 +ElW0BOQTKxCI/pqT+ehd4WP25u+RXBqOSfpIRQvY2RjXmeyrkDEZWATP/BUa/Oqa +0YitEsAXvt3pQli+LVc9GZSFZQECgwDVdAs4n7DdQlkLwIECQQDmFL9rIE/6wF3h +fJkvPFs67MJgMF/T4omLnv/FGSH7KBpjFHts9AbPIGjD1dadRpmHxk7ahbSTKMxu +uoZ1R1irAkEAx73MW4fJDQZDdJHwskYyGXuL99Fcr8xz6YZv75tm5O3eF2a/UvoO +UIgDGpTIWFrm+gli27p3J0rJhhOiI4JJwQJAYOjUR3bwuRlVcahdjTvK4WLf7Evz +0PdWH+z0pjwTyAn4M0tpQVb3lz57YiErqEsYV8v7Yqd2i5VfpjQCdlt6yQJBAIpm +7kph/SLEO0tzsGenEiHsJKFT9bhun8ape7h4YsSwOdrXPC0fzXlptVTe0S+/1Rpe +FJ0SSGv2e0snIYsfRUECQQCP8VOp3IIE8beytDoqn3QbWvobx94NVhHKUX5UB6C+ +bhY0LpTTFb8VMfSkICZXhbpcKf5zIdRjOh0ZLDeZJl5v +-----END RSA PRIVATE KEY----- diff --git a/examples/http2/static/index.html b/examples/http2/static/index.html new file mode 100644 index 0000000000..5ad7d424c8 --- /dev/null +++ b/examples/http2/static/index.html @@ -0,0 +1,16 @@ + + + + + + + + + +
+ Express with Http2 Example + Check out the network tab, notice how all the connections use the same connection ID + and the style gets via push (most of the time :) ) +
+ + \ No newline at end of file diff --git a/examples/http2/static/s1.js b/examples/http2/static/s1.js new file mode 100644 index 0000000000..48febb422d --- /dev/null +++ b/examples/http2/static/s1.js @@ -0,0 +1 @@ +console.log('Loaded'); diff --git a/examples/http2/static/s2.js b/examples/http2/static/s2.js new file mode 100644 index 0000000000..48febb422d --- /dev/null +++ b/examples/http2/static/s2.js @@ -0,0 +1 @@ +console.log('Loaded'); diff --git a/examples/http2/static/s3.js b/examples/http2/static/s3.js new file mode 100644 index 0000000000..48febb422d --- /dev/null +++ b/examples/http2/static/s3.js @@ -0,0 +1 @@ +console.log('Loaded'); diff --git a/examples/http2/static/style.css b/examples/http2/static/style.css new file mode 100644 index 0000000000..4c20fd42ad --- /dev/null +++ b/examples/http2/static/style.css @@ -0,0 +1,8 @@ +body { + margin: 0; + padding: 0; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/lib/application.js b/lib/application.js index 9d2495adbb..b87d8f65ae 100644 --- a/lib/application.js +++ b/lib/application.js @@ -19,6 +19,7 @@ var debug = require('debug')('express:application'); var View = require('./view'); var http = require('http'); var compileETag = require('./utils').compileETag; +var isHttp2Suported = require('./utils').isHttp2Supported; var compileQueryParser = require('./utils').compileQueryParser; var compileTrust = require('./utils').compileTrust; var flatten = require('array-flatten'); @@ -114,6 +115,11 @@ app.defaultConfiguration = function defaultConfiguration() { setPrototypeOf(this.response, parent.response) setPrototypeOf(this.engines, parent.engines) setPrototypeOf(this.settings, parent.settings) + // set prototype for http2 requests/response + if (isHttp2Suported) { + setPrototypeOf(this.http2Request, parent.http2Request) + setPrototypeOf(this.http2Response, parent.http2Response) + } }); // setup locals @@ -161,8 +167,13 @@ app.handle = function handle(req, res, callback) { res.req = req; // alter the prototypes - setPrototypeOf(req, this.request) - setPrototypeOf(res, this.response) + if (isHttp2Supported && req instanceof http2Request) { + setPrototypeOf(req, this.http2Request) + setPrototypeOf(res, this.http2Response) + } else { + setPrototypeOf(req, this.request) + setPrototypeOf(res, this.response) + } // setup locals if (!res.locals) { @@ -225,8 +236,13 @@ app.use = function use(fn) { router.use(path, function mounted_app(req, res, next) { var orig = req.app; fn.handle(req, res, function (err) { - setPrototypeOf(req, orig.request) - setPrototypeOf(res, orig.response) + if (typeof orig.http2Request !== 'undefined' && req instanceof http2Request) { + setPrototypeOf(req, orig.http2Request) + setPrototypeOf(res, orig.http2Response) + } else { + setPrototypeOf(req, orig.request) + setPrototypeOf(res, orig.response) + } next(err); }); }); diff --git a/lib/express.js b/lib/express.js index fca79a3383..25035aaf21 100644 --- a/lib/express.js +++ b/lib/express.js @@ -19,7 +19,7 @@ var proto = require('./application'); var Router = require('router'); var req = require('./request'); var res = require('./response'); - +var isHttp2Supported = require('./utils').isHttp2Supported; /** * Expose `createApplication()`. */ @@ -34,7 +34,7 @@ exports = module.exports = createApplication; */ function createApplication() { - var app = function(req, res, next) { + var app = function (req, res, next) { app.handle(req, res, next); }; @@ -51,6 +51,21 @@ function createApplication() { app: { configurable: true, enumerable: true, writable: true, value: app } }) + if (isHttp2Supported) { + var http2Req = require('./http2Request'); + var http2Res = require('./http2Response'); + app.http2Request = Object.create(http2Req, { + app: { configurable: true, enumerable: true, writable: true, value: app } + }); + + app.http2Response = Object.create(http2Res, { + app: { configurable: true, enumerable: true, writable: true, value: app } + }); + } + + + + app.init(); return app; } diff --git a/lib/http2Request.js b/lib/http2Request.js new file mode 100644 index 0000000000..733c095ba7 --- /dev/null +++ b/lib/http2Request.js @@ -0,0 +1,23 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2013 Roman Shtylman + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ +var http2 = require('http2'); +var decorator = require('./requestDecorator'); +var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); +/** + * Module exports. + * @public + */ + +module.exports = http2Req; diff --git a/lib/http2Response.js b/lib/http2Response.js new file mode 100644 index 0000000000..27dcd7b3d2 --- /dev/null +++ b/lib/http2Response.js @@ -0,0 +1,29 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ +var Http2ServerResponse = require('http2').Http2ServerResponse; +var decorator = require('./responseDecorator'); +/** + * Response prototype. + * @public + */ + +var res = decorator(Object.create(Http2ServerResponse.prototype)); + +/** + * Module exports. + * @public + */ + +module.exports = res; + diff --git a/lib/request.js b/lib/request.js index c652714fb3..332ab7bb6d 100644 --- a/lib/request.js +++ b/lib/request.js @@ -12,500 +12,18 @@ * Module dependencies. * @private */ - -var accepts = require('accepts'); -var isIP = require('net').isIP; -var typeis = require('type-is'); +var decorator = require('./requestDecorator'); var http = require('http'); -var fresh = require('fresh'); -var parseRange = require('range-parser'); -var parse = require('parseurl'); -var proxyaddr = require('proxy-addr'); /** * Request prototype. * @public */ -var req = Object.create(http.IncomingMessage.prototype) - +var req = decorator(Object.create(http.IncomingMessage.prototype)); /** * Module exports. * @public */ -module.exports = req - -/** - * Return request header. - * - * The `Referrer` header field is special-cased, - * both `Referrer` and `Referer` are interchangeable. - * - * Examples: - * - * req.get('Content-Type'); - * // => "text/plain" - * - * req.get('content-type'); - * // => "text/plain" - * - * req.get('Something'); - * // => undefined - * - * Aliased as `req.header()`. - * - * @param {String} name - * @return {String} - * @public - */ - -req.get = -req.header = function header(name) { - if (!name) { - throw new TypeError('name argument is required to req.get'); - } - - if (typeof name !== 'string') { - throw new TypeError('name must be a string to req.get'); - } - - var lc = name.toLowerCase(); - - switch (lc) { - case 'referer': - case 'referrer': - return this.headers.referrer - || this.headers.referer; - default: - return this.headers[lc]; - } -}; - -/** - * To do: update docs. - * - * Check if the given `type(s)` is acceptable, returning - * the best match when true, otherwise `undefined`, in which - * case you should respond with 406 "Not Acceptable". - * - * The `type` value may be a single MIME type string - * such as "application/json", an extension name - * such as "json", a comma-delimited list such as "json, html, text/plain", - * an argument list such as `"json", "html", "text/plain"`, - * or an array `["json", "html", "text/plain"]`. When a list - * or array is given, the _best_ match, if any is returned. - * - * Examples: - * - * // Accept: text/html - * req.accepts('html'); - * // => "html" - * - * // Accept: text/*, application/json - * req.accepts('html'); - * // => "html" - * req.accepts('text/html'); - * // => "text/html" - * req.accepts('json, text'); - * // => "json" - * req.accepts('application/json'); - * // => "application/json" - * - * // Accept: text/*, application/json - * req.accepts('image/png'); - * req.accepts('png'); - * // => undefined - * - * // Accept: text/*;q=.5, application/json - * req.accepts(['html', 'json']); - * req.accepts('html', 'json'); - * req.accepts('html, json'); - * // => "json" - * - * @param {String|Array} type(s) - * @return {String|Array|Boolean} - * @public - */ - -req.accepts = function(){ - var accept = accepts(this); - return accept.types.apply(accept, arguments); -}; - -/** - * Check if the given `encoding`s are accepted. - * - * @param {String} ...encoding - * @return {String|Array} - * @public - */ - -req.acceptsEncodings = function(){ - var accept = accepts(this); - return accept.encodings.apply(accept, arguments); -}; - -/** - * Check if the given `charset`s are acceptable, - * otherwise you should respond with 406 "Not Acceptable". - * - * @param {String} ...charset - * @return {String|Array} - * @public - */ - -req.acceptsCharsets = function(){ - var accept = accepts(this); - return accept.charsets.apply(accept, arguments); -}; - -/** - * Check if the given `lang`s are acceptable, - * otherwise you should respond with 406 "Not Acceptable". - * - * @param {String} ...lang - * @return {String|Array} - * @public - */ - -req.acceptsLanguages = function(){ - var accept = accepts(this); - return accept.languages.apply(accept, arguments); -}; - -/** - * Parse Range header field, capping to the given `size`. - * - * Unspecified ranges such as "0-" require knowledge of your resource length. In - * the case of a byte range this is of course the total number of bytes. If the - * Range header field is not given `undefined` is returned, `-1` when unsatisfiable, - * and `-2` when syntactically invalid. - * - * When ranges are returned, the array has a "type" property which is the type of - * range that is required (most commonly, "bytes"). Each array element is an object - * with a "start" and "end" property for the portion of the range. - * - * The "combine" option can be set to `true` and overlapping & adjacent ranges - * will be combined into a single range. - * - * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" - * should respond with 4 users when available, not 3. - * - * @param {number} size - * @param {object} [options] - * @param {boolean} [options.combine=false] - * @return {number|array} - * @public - */ - -req.range = function range(size, options) { - var range = this.get('Range'); - if (!range) return; - return parseRange(size, range, options); -}; - -/** - * Parse the query string of `req.url`. - * - * This uses the "query parser" setting to parse the raw - * string into an object. - * - * @return {String} - * @api public - */ - -defineGetter(req, 'query', function query(){ - var queryparse = this.app.get('query parser fn'); - - if (!queryparse) { - // parsing is disabled - return Object.create(null); - } - - var querystring = parse(this).query; - - return queryparse(querystring); -}); - -/** - * Check if the incoming request contains the "Content-Type" - * header field, and it contains the give mime `type`. - * - * Examples: - * - * // With Content-Type: text/html; charset=utf-8 - * req.is('html'); - * req.is('text/html'); - * req.is('text/*'); - * // => true - * - * // When Content-Type is application/json - * req.is('json'); - * req.is('application/json'); - * req.is('application/*'); - * // => true - * - * req.is('html'); - * // => false - * - * @param {String|Array} types... - * @return {String|false|null} - * @public - */ - -req.is = function is(types) { - var arr = types; - - // support flattened arguments - if (!Array.isArray(types)) { - arr = new Array(arguments.length); - for (var i = 0; i < arr.length; i++) { - arr[i] = arguments[i]; - } - } - - return typeis(this, arr); -}; - -/** - * Return the protocol string "http" or "https" - * when requested with TLS. When the "trust proxy" - * setting trusts the socket address, the - * "X-Forwarded-Proto" header field will be trusted - * and used if present. - * - * If you're running behind a reverse proxy that - * supplies https for you this may be enabled. - * - * @return {String} - * @public - */ - -defineGetter(req, 'protocol', function protocol(){ - var proto = this.connection.encrypted - ? 'https' - : 'http'; - var trust = this.app.get('trust proxy fn'); - - if (!trust(this.connection.remoteAddress, 0)) { - return proto; - } - - // Note: X-Forwarded-Proto is normally only ever a - // single value, but this is to be safe. - var header = this.get('X-Forwarded-Proto') || proto - var index = header.indexOf(',') - - return index !== -1 - ? header.substring(0, index).trim() - : header.trim() -}); - -/** - * Short-hand for: - * - * req.protocol === 'https' - * - * @return {Boolean} - * @public - */ - -defineGetter(req, 'secure', function secure(){ - return this.protocol === 'https'; -}); - -/** - * Return the remote address from the trusted proxy. - * - * The is the remote address on the socket unless - * "trust proxy" is set. - * - * @return {String} - * @public - */ - -defineGetter(req, 'ip', function ip(){ - var trust = this.app.get('trust proxy fn'); - return proxyaddr(this, trust); -}); - -/** - * When "trust proxy" is set, trusted proxy addresses + client. - * - * For example if the value were "client, proxy1, proxy2" - * you would receive the array `["client", "proxy1", "proxy2"]` - * where "proxy2" is the furthest down-stream and "proxy1" and - * "proxy2" were trusted. - * - * @return {Array} - * @public - */ - -defineGetter(req, 'ips', function ips() { - var trust = this.app.get('trust proxy fn'); - var addrs = proxyaddr.all(this, trust); - - // reverse the order (to farthest -> closest) - // and remove socket address - addrs.reverse().pop() - - return addrs -}); - -/** - * Return subdomains as an array. - * - * Subdomains are the dot-separated parts of the host before the main domain of - * the app. By default, the domain of the app is assumed to be the last two - * parts of the host. This can be changed by setting "subdomain offset". - * - * For example, if the domain is "tobi.ferrets.example.com": - * If "subdomain offset" is not set, req.subdomains is `["ferrets", "tobi"]`. - * If "subdomain offset" is 3, req.subdomains is `["tobi"]`. - * - * @return {Array} - * @public - */ - -defineGetter(req, 'subdomains', function subdomains() { - var hostname = this.hostname; - - if (!hostname) return []; - - var offset = this.app.get('subdomain offset'); - var subdomains = !isIP(hostname) - ? hostname.split('.').reverse() - : [hostname]; - - return subdomains.slice(offset); -}); - -/** - * Short-hand for `url.parse(req.url).pathname`. - * - * @return {String} - * @public - */ - -defineGetter(req, 'path', function path() { - return parse(this).pathname; -}); - -/** - * Parse the "Host" header field to a host. - * - * When the "trust proxy" setting trusts the socket - * address, the "X-Forwarded-Host" header field will - * be trusted. - * - * @return {String} - * @public - */ - -defineGetter(req, 'host', function host(){ - var trust = this.app.get('trust proxy fn'); - var val = this.get('X-Forwarded-Host'); - - if (!val || !trust(this.connection.remoteAddress, 0)) { - val = this.get('Host'); - } - - return val || undefined; -}); - -/** - * Parse the "Host" header field to a hostname. - * - * When the "trust proxy" setting trusts the socket - * address, the "X-Forwarded-Host" header field will - * be trusted. - * - * @return {String} - * @api public - */ - -defineGetter(req, 'hostname', function hostname(){ - var host = this.host; - - if (!host) return; - - // IPv6 literal support - var offset = host[0] === '[' - ? host.indexOf(']') + 1 - : 0; - var index = host.indexOf(':', offset); - - return index !== -1 - ? host.substring(0, index) - : host; -}); - -/** - * Check if the request is fresh, aka - * Last-Modified and/or the ETag - * still match. - * - * @return {Boolean} - * @public - */ - -defineGetter(req, 'fresh', function(){ - var method = this.method; - var res = this.res - var status = res.statusCode - - // GET or HEAD for weak freshness validation only - if ('GET' !== method && 'HEAD' !== method) return false; - - // 2xx or 304 as per rfc2616 14.26 - if ((status >= 200 && status < 300) || 304 === status) { - return fresh(this.headers, { - 'etag': res.get('ETag'), - 'last-modified': res.get('Last-Modified') - }) - } - - return false; -}); - -/** - * Check if the request is stale, aka - * "Last-Modified" and / or the "ETag" for the - * resource has changed. - * - * @return {Boolean} - * @public - */ - -defineGetter(req, 'stale', function stale(){ - return !this.fresh; -}); - -/** - * Check if the request was an _XMLHttpRequest_. - * - * @return {Boolean} - * @public - */ - -defineGetter(req, 'xhr', function xhr(){ - var val = this.get('X-Requested-With') || ''; - return val.toLowerCase() === 'xmlhttprequest'; -}); - -/** - * Helper function for creating a getter on an object. - * - * @param {Object} obj - * @param {String} name - * @param {Function} getter - * @private - */ -function defineGetter(obj, name, getter) { - Object.defineProperty(obj, name, { - configurable: true, - enumerable: true, - get: getter - }); -} +module.exports = req; diff --git a/lib/requestDecorator.js b/lib/requestDecorator.js new file mode 100644 index 0000000000..996613261c --- /dev/null +++ b/lib/requestDecorator.js @@ -0,0 +1,518 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2013 Roman Shtylman + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var accepts = require('accepts'); +var deprecate = require('depd')('express'); +var isIP = require('net').isIP; +var typeis = require('type-is'); +var fresh = require('fresh'); +var parseRange = require('range-parser'); +var parse = require('parseurl'); +var proxyaddr = require('proxy-addr'); + +/** + * Request prototype. + * @public + */ + +/** + * Module exports. + * @public + */ + +module.exports = setMethods; + +function setMethods(req) { + + /** + * Return request header. + * + * The `Referrer` header field is special-cased, + * both `Referrer` and `Referer` are interchangeable. + * + * Examples: + * + * req.get('Content-Type'); + * // => "text/plain" + * + * req.get('content-type'); + * // => "text/plain" + * + * req.get('Something'); + * // => undefined + * + * Aliased as `req.header()`. + * + * @param {String} name + * @return {String} + * @public + */ + + req.get = + req.header = function header(name) { + if (!name) { + throw new TypeError('name argument is required to req.get'); + } + + if (typeof name !== 'string') { + throw new TypeError('name must be a string to req.get'); + } + + var lc = name.toLowerCase(); + + switch (lc) { + case 'referer': + case 'referrer': + return this.headers.referrer + || this.headers.referer; + default: + return this.headers[lc]; + } + }; + + /** + * To do: update docs. + * + * Check if the given `type(s)` is acceptable, returning + * the best match when true, otherwise `undefined`, in which + * case you should respond with 406 "Not Acceptable". + * + * The `type` value may be a single MIME type string + * such as "application/json", an extension name + * such as "json", a comma-delimited list such as "json, html, text/plain", + * an argument list such as `"json", "html", "text/plain"`, + * or an array `["json", "html", "text/plain"]`. When a list + * or array is given, the _best_ match, if any is returned. + * + * Examples: + * + * // Accept: text/html + * req.accepts('html'); + * // => "html" + * + * // Accept: text/*, application/json + * req.accepts('html'); + * // => "html" + * req.accepts('text/html'); + * // => "text/html" + * req.accepts('json, text'); + * // => "json" + * req.accepts('application/json'); + * // => "application/json" + * + * // Accept: text/*, application/json + * req.accepts('image/png'); + * req.accepts('png'); + * // => undefined + * + * // Accept: text/*;q=.5, application/json + * req.accepts(['html', 'json']); + * req.accepts('html', 'json'); + * req.accepts('html, json'); + * // => "json" + * + * @param {String|Array} type(s) + * @return {String|Array|Boolean} + * @public + */ + + req.accepts = function () { + var accept = accepts(this); + return accept.types.apply(accept, arguments); + }; + + /** + * Check if the given `encoding`s are accepted. + * + * @param {String} ...encoding + * @return {String|Array} + * @public + */ + + req.acceptsEncodings = function () { + var accept = accepts(this); + return accept.encodings.apply(accept, arguments); + }; + + req.acceptsEncoding = deprecate.function(req.acceptsEncodings, + 'req.acceptsEncoding: Use acceptsEncodings instead'); + + /** + * Check if the given `charset`s are acceptable, + * otherwise you should respond with 406 "Not Acceptable". + * + * @param {String} ...charset + * @return {String|Array} + * @public + */ + + req.acceptsCharsets = function () { + var accept = accepts(this); + return accept.charsets.apply(accept, arguments); + }; + + /** + * Check if the given `lang`s are acceptable, + * otherwise you should respond with 406 "Not Acceptable". + * + * @param {String} ...lang + * @return {String|Array} + * @public + */ + + req.acceptsLanguages = function () { + var accept = accepts(this); + return accept.languages.apply(accept, arguments); + }; + + /** + * Parse Range header field, capping to the given `size`. + * + * Unspecified ranges such as "0-" require knowledge of your resource length. In + * the case of a byte range this is of course the total number of bytes. If the + * Range header field is not given `undefined` is returned, `-1` when unsatisfiable, + * and `-2` when syntactically invalid. + * + * When ranges are returned, the array has a "type" property which is the type of + * range that is required (most commonly, "bytes"). Each array element is an object + * with a "start" and "end" property for the portion of the range. + * + * The "combine" option can be set to `true` and overlapping & adjacent ranges + * will be combined into a single range. + * + * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" + * should respond with 4 users when available, not 3. + * + * @param {number} size + * @param {object} [options] + * @param {boolean} [options.combine=false] + * @return {number|array} + * @public + */ + + req.range = function range(size, options) { + var range = this.get('Range'); + if (!range) return; + return parseRange(size, range, options); + }; + + /** + * Parse the query string of `req.url`. + * + * This uses the "query parser" setting to parse the raw + * string into an object. + * + * @return {String} + * @api public + */ + + defineGetter(req, 'query', function query(){ + var queryparse = this.app.get('query parser fn'); + + if (!queryparse) { + // parsing is disabled + return Object.create(null); + } + + var querystring = parse(this).query; + + return queryparse(querystring); + }); + + /** + * Check if the incoming request contains the "Content-Type" + * header field, and it contains the give mime `type`. + * + * Examples: + * + * // With Content-Type: text/html; charset=utf-8 + * req.is('html'); + * req.is('text/html'); + * req.is('text/*'); + * // => true + * + * // When Content-Type is application/json + * req.is('json'); + * req.is('application/json'); + * req.is('application/*'); + * // => true + * + * req.is('html'); + * // => false + * + * @param {String|Array} types... + * @return {String|false|null} + * @public + */ + + req.is = function is(types) { + var arr = types; + + // support flattened arguments + if (!Array.isArray(types)) { + arr = new Array(arguments.length); + for (var i = 0; i < arr.length; i++) { + arr[i] = arguments[i]; + } + } + + return typeis(this, arr); + }; + + /** + * Return the protocol string "http" or "https" + * when requested with TLS. When the "trust proxy" + * setting trusts the socket address, the + * "X-Forwarded-Proto" header field will be trusted + * and used if present. + * + * If you're running behind a reverse proxy that + * supplies https for you this may be enabled. + * + * @return {String} + * @public + */ + + defineGetter(req, 'protocol', function protocol() { + var proto = this.connection.encrypted + ? 'https' + : 'http'; + var trust = this.app.get('trust proxy fn'); + + if (!trust(this.connection.remoteAddress, 0)) { + return proto; + } + + // Note: X-Forwarded-Proto is normally only ever a + // single value, but this is to be safe. + var header = this.get('X-Forwarded-Proto') || proto + var index = header.indexOf(',') + + return index !== -1 + ? header.substring(0, index).trim() + : header.trim() + }); + + /** + * Short-hand for: + * + * req.protocol === 'https' + * + * @return {Boolean} + * @public + */ + + defineGetter(req, 'secure', function secure() { + return this.protocol === 'https'; + }); + + /** + * Return the remote address from the trusted proxy. + * + * The is the remote address on the socket unless + * "trust proxy" is set. + * + * @return {String} + * @public + */ + + defineGetter(req, 'ip', function ip() { + var trust = this.app.get('trust proxy fn'); + return proxyaddr(this, trust); + }); + + /** + * When "trust proxy" is set, trusted proxy addresses + client. + * + * For example if the value were "client, proxy1, proxy2" + * you would receive the array `["client", "proxy1", "proxy2"]` + * where "proxy2" is the furthest down-stream and "proxy1" and + * "proxy2" were trusted. + * + * @return {Array} + * @public + */ + + defineGetter(req, 'ips', function ips() { + var trust = this.app.get('trust proxy fn'); + var addrs = proxyaddr.all(this, trust); + + // reverse the order (to farthest -> closest) + // and remove socket address + addrs.reverse().pop() + + return addrs + }); + + /** + * Return subdomains as an array. + * + * Subdomains are the dot-separated parts of the host before the main domain of + * the app. By default, the domain of the app is assumed to be the last two + * parts of the host. This can be changed by setting "subdomain offset". + * + * For example, if the domain is "tobi.ferrets.example.com": + * If "subdomain offset" is not set, req.subdomains is `["ferrets", "tobi"]`. + * If "subdomain offset" is 3, req.subdomains is `["tobi"]`. + * + * @return {Array} + * @public + */ + + defineGetter(req, 'subdomains', function subdomains() { + var hostname = this.hostname; + + if (!hostname) return []; + + var offset = this.app.get('subdomain offset'); + var subdomains = !isIP(hostname) + ? hostname.split('.').reverse() + : [hostname]; + + return subdomains.slice(offset); + }); + + /** + * Short-hand for `url.parse(req.url).pathname`. + * + * @return {String} + * @public + */ + + defineGetter(req, 'path', function path() { + return parse(this).pathname; + }); + + /** + * Parse the "Host" header field to a host. + * + * When the "trust proxy" setting trusts the socket + * address, the "X-Forwarded-Host" header field will + * be trusted. + * + * @return {String} + * @public + */ + + defineGetter(req, 'host', function host() { + var trust = this.app.get('trust proxy fn'); + var val = this.get('X-Forwarded-Host'); + + if (!val || !trust(this.connection.remoteAddress, 0)) { + val = this.get('Host'); + } + + return val || undefined; + }); + + /** + * Parse the "Host" header field to a hostname. + * + * When the "trust proxy" setting trusts the socket + * address, the "X-Forwarded-Host" header field will + * be trusted. + * + * @return {String} + * @api public + */ + + defineGetter(req, 'hostname', function hostname(){ + var host = this.host; + + if (!host) return; + + // IPv6 literal support + var offset = host[0] === '[' + ? host.indexOf(']') + 1 + : 0; + var index = host.indexOf(':', offset); + + return index !== -1 + ? host.substring(0, index) + : host; + }); + + /** + * Check if the request is fresh, aka + * Last-Modified and/or the ETag + * still match. + * + * @return {Boolean} + * @public + */ + + defineGetter(req, 'fresh', function () { + var method = this.method; + var res = this.res + var status = res.statusCode + + // GET or HEAD for weak freshness validation only + if ('GET' !== method && 'HEAD' !== method) return false; + + // 2xx or 304 as per rfc2616 14.26 + if ((status >= 200 && status < 300) || 304 === status) { + return fresh(this.headers, { + 'etag': res.get('ETag'), + 'last-modified': res.get('Last-Modified') + }) + } + + return false; + }); + + /** + * Check if the request is stale, aka + * "Last-Modified" and / or the "ETag" for the + * resource has changed. + * + * @return {Boolean} + * @public + */ + + defineGetter(req, 'stale', function stale() { + return !this.fresh; + }); + + /** + * Check if the request was an _XMLHttpRequest_. + * + * @return {Boolean} + * @public + */ + + defineGetter(req, 'xhr', function xhr() { + var val = this.get('X-Requested-With') || ''; + return val.toLowerCase() === 'xmlhttprequest'; + }); + + return req; +} + +/** + * Helper function for creating a getter on an object. + * + * @param {Object} obj + * @param {String} name + * @param {Function} getter + * @private + */ + +function defineGetter(obj, name, getter) { + Object.defineProperty(obj, name, { + configurable: true, + enumerable: true, + get: getter + }); +} diff --git a/lib/response.js b/lib/response.js index 1d4f9a6f17..24fd99bb02 100644 --- a/lib/response.js +++ b/lib/response.js @@ -11,988 +11,17 @@ * Module dependencies. * @private */ - -var Buffer = require('safe-buffer').Buffer -var contentDisposition = require('content-disposition'); -var encodeUrl = require('encodeurl'); -var escapeHtml = require('escape-html'); var http = require('http'); -var onFinished = require('on-finished'); -var path = require('path'); -var pathIsAbsolute = require('path-is-absolute'); -var statuses = require('statuses') -var merge = require('utils-merge'); -var sign = require('cookie-signature').sign; -var normalizeType = require('./utils').normalizeType; -var normalizeTypes = require('./utils').normalizeTypes; -var setCharset = require('./utils').setCharset; -var cookie = require('cookie'); -var send = require('send'); -var extname = path.extname; -var mime = send.mime; -var resolve = path.resolve; -var vary = require('vary'); - +var decorator = require('./responseDecorator'); /** * Response prototype. * @public */ -var res = Object.create(http.ServerResponse.prototype) - +var res = decorator(Object.create(http.ServerResponse.prototype)); /** * Module exports. * @public */ -module.exports = res - -/** - * Module variables. - * @private - */ - -var charsetRegExp = /;\s*charset\s*=/; - -/** - * Set status `code`. - * - * @param {Number} code - * @return {ServerResponse} - * @public - */ - -res.status = function status(code) { - this.statusCode = code; - return this; -}; - -/** - * Set Link header field with the given `links`. - * - * Examples: - * - * res.links({ - * next: 'http://api.example.com/users?page=2', - * last: 'http://api.example.com/users?page=5' - * }); - * - * @param {Object} links - * @return {ServerResponse} - * @public - */ - -res.links = function(links){ - var link = this.get('Link') || ''; - if (link) link += ', '; - return this.set('Link', link + Object.keys(links).map(function(rel){ - return '<' + links[rel] + '>; rel="' + rel + '"'; - }).join(', ')); -}; - -/** - * Send a response. - * - * Examples: - * - * res.send(Buffer.from('wahoo')); - * res.send({ some: 'json' }); - * res.send('

some html

'); - * - * @param {string|number|boolean|object|Buffer} body - * @public - */ - -res.send = function send(body) { - var chunk = body; - var encoding; - var req = this.req; - var type; - - // settings - var app = this.app; - - switch (typeof chunk) { - // string defaulting to html - case 'string': - if (!this.get('Content-Type')) { - this.type('html'); - } - break; - case 'boolean': - case 'number': - case 'object': - if (chunk === null) { - chunk = ''; - } else if (Buffer.isBuffer(chunk)) { - if (!this.get('Content-Type')) { - this.type('bin'); - } - } else { - return this.json(chunk); - } - break; - } - - // write strings in utf-8 - if (typeof chunk === 'string') { - encoding = 'utf8'; - type = this.get('Content-Type'); - - // reflect this in content-type - if (typeof type === 'string') { - this.set('Content-Type', setCharset(type, 'utf-8')); - } - } - - // determine if ETag should be generated - var etagFn = app.get('etag fn') - var generateETag = !this.get('ETag') && typeof etagFn === 'function' - - // populate Content-Length - var len - if (chunk !== undefined) { - if (Buffer.isBuffer(chunk)) { - // get length of Buffer - len = chunk.length - } else if (!generateETag && chunk.length < 1000) { - // just calculate length when no ETag + small chunk - len = Buffer.byteLength(chunk, encoding) - } else { - // convert chunk to Buffer and calculate - chunk = Buffer.from(chunk, encoding) - encoding = undefined; - len = chunk.length - } - - this.set('Content-Length', len); - } - - // populate ETag - var etag; - if (generateETag && len !== undefined) { - if ((etag = etagFn(chunk, encoding))) { - this.set('ETag', etag); - } - } - - // freshness - if (req.fresh) this.statusCode = 304; - - // strip irrelevant headers - if (204 === this.statusCode || 304 === this.statusCode) { - this.removeHeader('Content-Type'); - this.removeHeader('Content-Length'); - this.removeHeader('Transfer-Encoding'); - chunk = ''; - } - - if (req.method === 'HEAD') { - // skip body for HEAD - this.end(); - } else { - // respond - this.end(chunk, encoding); - } - - return this; -}; - -/** - * Send JSON response. - * - * Examples: - * - * res.json(null); - * res.json({ user: 'tj' }); - * - * @param {string|number|boolean|object} obj - * @public - */ - -res.json = function json(obj) { - // settings - var app = this.app; - var escape = app.get('json escape') - var replacer = app.get('json replacer'); - var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape) - - // content-type - if (!this.get('Content-Type')) { - this.set('Content-Type', 'application/json'); - } - - return this.send(body); -}; - -/** - * Send JSON response with JSONP callback support. - * - * Examples: - * - * res.jsonp(null); - * res.jsonp({ user: 'tj' }); - * - * @param {string|number|boolean|object} obj - * @public - */ - -res.jsonp = function jsonp(obj) { - // settings - var app = this.app; - var escape = app.get('json escape') - var replacer = app.get('json replacer'); - var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape) - var callback = this.req.query[app.get('jsonp callback name')]; - - // content-type - if (!this.get('Content-Type')) { - this.set('X-Content-Type-Options', 'nosniff'); - this.set('Content-Type', 'application/json'); - } - - // fixup callback - if (Array.isArray(callback)) { - callback = callback[0]; - } - - // jsonp - if (typeof callback === 'string' && callback.length !== 0) { - this.set('X-Content-Type-Options', 'nosniff'); - this.set('Content-Type', 'text/javascript'); - - // restrict callback charset - callback = callback.replace(/[^\[\]\w$.]/g, ''); - - // replace chars not allowed in JavaScript that are in JSON - body = body - .replace(/\u2028/g, '\\u2028') - .replace(/\u2029/g, '\\u2029'); - - // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" - // the typeof check is just to reduce client error noise - body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');'; - } - - return this.send(body); -}; - -/** - * Send given HTTP status code. - * - * Sets the response status to `statusCode` and the body of the - * response to the standard description from node's http.STATUS_CODES - * or the statusCode number if no description. - * - * Examples: - * - * res.sendStatus(200); - * - * @param {number} statusCode - * @public - */ - -res.sendStatus = function sendStatus(statusCode) { - var body = statuses[statusCode] || String(statusCode) - - this.statusCode = statusCode; - this.type('txt'); - - return this.send(body); -}; - -/** - * Transfer the file at the given `path`. - * - * Automatically sets the _Content-Type_ response header field. - * The callback `callback(err)` is invoked when the transfer is complete - * or when an error occurs. Be sure to check `res.sentHeader` - * if you wish to attempt responding, as the header and some data - * may have already been transferred. - * - * Options: - * - * - `maxAge` defaulting to 0 (can be string converted by `ms`) - * - `root` root directory for relative filenames - * - `headers` object of headers to serve with file - * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them - * - * Other options are passed along to `send`. - * - * Examples: - * - * The following example illustrates how `res.sendFile()` may - * be used as an alternative for the `static()` middleware for - * dynamic situations. The code backing `res.sendFile()` is actually - * the same code, so HTTP cache support etc is identical. - * - * app.get('/user/:uid/photos/:file', function(req, res){ - * var uid = req.params.uid - * , file = req.params.file; - * - * req.user.mayViewFilesFrom(uid, function(yes){ - * if (yes) { - * res.sendFile('/uploads/' + uid + '/' + file); - * } else { - * res.send(403, 'Sorry! you cant see that.'); - * } - * }); - * }); - * - * @public - */ - -res.sendFile = function sendFile(path, options, callback) { - var done = callback; - var req = this.req; - var res = this; - var next = req.next; - var opts = options || {}; - - if (!path) { - throw new TypeError('path argument is required to res.sendFile'); - } - - // support function as second arg - if (typeof options === 'function') { - done = options; - opts = {}; - } - - if (!opts.root && !pathIsAbsolute(path)) { - throw new TypeError('path must be absolute or specify root to res.sendFile'); - } - - // create file stream - var pathname = encodeURI(path); - var file = send(req, pathname, opts); - - // transfer - sendfile(res, file, opts, function (err) { - if (done) return done(err); - if (err && err.code === 'EISDIR') return next(); - - // next() all but write errors - if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') { - next(err); - } - }); -}; - -/** - * Transfer the file at the given `path` as an attachment. - * - * Optionally providing an alternate attachment `filename`, - * and optional callback `callback(err)`. The callback is invoked - * when the data transfer is complete, or when an error has - * ocurred. Be sure to check `res.headersSent` if you plan to respond. - * - * Optionally providing an `options` object to use with `res.sendFile()`. - * This function will set the `Content-Disposition` header, overriding - * any `Content-Disposition` header passed as header options in order - * to set the attachment and filename. - * - * This method uses `res.sendFile()`. - * - * @public - */ - -res.download = function download (path, filename, options, callback) { - var done = callback; - var name = filename; - var opts = options || null - - // support function as second or third arg - if (typeof filename === 'function') { - done = filename; - name = null; - opts = null - } else if (typeof options === 'function') { - done = options - opts = null - } - - // set Content-Disposition when file is sent - var headers = { - 'Content-Disposition': contentDisposition(name || path) - }; - - // merge user-provided headers - if (opts && opts.headers) { - var keys = Object.keys(opts.headers) - for (var i = 0; i < keys.length; i++) { - var key = keys[i] - if (key.toLowerCase() !== 'content-disposition') { - headers[key] = opts.headers[key] - } - } - } - - // merge user-provided options - opts = Object.create(opts) - opts.headers = headers - - // Resolve the full path for sendFile - var fullPath = resolve(path); - - // send file - return this.sendFile(fullPath, opts, done) -}; - -/** - * Set _Content-Type_ response header with `type` through `mime.lookup()` - * when it does not contain "/", or set the Content-Type to `type` otherwise. - * - * Examples: - * - * res.type('.html'); - * res.type('html'); - * res.type('json'); - * res.type('application/json'); - * res.type('png'); - * - * @param {String} type - * @return {ServerResponse} for chaining - * @public - */ - -res.contentType = -res.type = function contentType(type) { - var ct = type.indexOf('/') === -1 - ? mime.lookup(type) - : type; - - return this.set('Content-Type', ct); -}; - -/** - * Respond to the Acceptable formats using an `obj` - * of mime-type callbacks. - * - * This method uses `req.accepted`, an array of - * acceptable types ordered by their quality values. - * When "Accept" is not present the _first_ callback - * is invoked, otherwise the first match is used. When - * no match is performed the server responds with - * 406 "Not Acceptable". - * - * Content-Type is set for you, however if you choose - * you may alter this within the callback using `res.type()` - * or `res.set('Content-Type', ...)`. - * - * res.format({ - * 'text/plain': function(){ - * res.send('hey'); - * }, - * - * 'text/html': function(){ - * res.send('

hey

'); - * }, - * - * 'appliation/json': function(){ - * res.send({ message: 'hey' }); - * } - * }); - * - * In addition to canonicalized MIME types you may - * also use extnames mapped to these types: - * - * res.format({ - * text: function(){ - * res.send('hey'); - * }, - * - * html: function(){ - * res.send('

hey

'); - * }, - * - * json: function(){ - * res.send({ message: 'hey' }); - * } - * }); - * - * By default Express passes an `Error` - * with a `.status` of 406 to `next(err)` - * if a match is not made. If you provide - * a `.default` callback it will be invoked - * instead. - * - * @param {Object} obj - * @return {ServerResponse} for chaining - * @public - */ - -res.format = function(obj){ - var req = this.req; - var next = req.next; - - var fn = obj.default; - if (fn) delete obj.default; - var keys = Object.keys(obj); - - var key = keys.length > 0 - ? req.accepts(keys) - : false; - - this.vary("Accept"); - - if (key) { - this.set('Content-Type', normalizeType(key).value); - obj[key](req, this, next); - } else if (fn) { - fn(); - } else { - var err = new Error('Not Acceptable'); - err.status = err.statusCode = 406; - err.types = normalizeTypes(keys).map(function(o){ return o.value }); - next(err); - } - - return this; -}; - -/** - * Set _Content-Disposition_ header to _attachment_ with optional `filename`. - * - * @param {String} filename - * @return {ServerResponse} - * @public - */ - -res.attachment = function attachment(filename) { - if (filename) { - this.type(extname(filename)); - } - - this.set('Content-Disposition', contentDisposition(filename)); - - return this; -}; - -/** - * Append additional header `field` with value `val`. - * - * Example: - * - * res.append('Link', ['', '']); - * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); - * res.append('Warning', '199 Miscellaneous warning'); - * - * @param {String} field - * @param {String|Array} val - * @return {ServerResponse} for chaining - * @public - */ - -res.append = function append(field, val) { - var prev = this.get(field); - var value = val; - - if (prev) { - // concat the new and prev vals - value = Array.isArray(prev) ? prev.concat(val) - : Array.isArray(val) ? [prev].concat(val) - : [prev, val]; - } - - return this.set(field, value); -}; - -/** - * Set header `field` to `val`, or pass - * an object of header fields. - * - * Examples: - * - * res.set('Foo', ['bar', 'baz']); - * res.set('Accept', 'application/json'); - * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); - * - * Aliased as `res.header()`. - * - * @param {String|Object} field - * @param {String|Array} val - * @return {ServerResponse} for chaining - * @public - */ - -res.set = -res.header = function header(field, val) { - if (arguments.length === 2) { - var value = Array.isArray(val) - ? val.map(String) - : String(val); - - // add charset to content-type - if (field.toLowerCase() === 'content-type') { - if (Array.isArray(value)) { - throw new TypeError('Content-Type cannot be set to an Array'); - } - if (!charsetRegExp.test(value)) { - var charset = mime.charsets.lookup(value.split(';')[0]); - if (charset) value += '; charset=' + charset.toLowerCase(); - } - } - - this.setHeader(field, value); - } else { - for (var key in field) { - this.set(key, field[key]); - } - } - return this; -}; - -/** - * Get value for header `field`. - * - * @param {String} field - * @return {String} - * @public - */ - -res.get = function(field){ - return this.getHeader(field); -}; - -/** - * Clear cookie `name`. - * - * @param {String} name - * @param {Object} [options] - * @return {ServerResponse} for chaining - * @public - */ - -res.clearCookie = function clearCookie(name, options) { - var opts = merge({ expires: new Date(1), path: '/' }, options); - - return this.cookie(name, '', opts); -}; - -/** - * Set cookie `name` to `value`, with the given `options`. - * - * Options: - * - * - `maxAge` max-age in milliseconds, converted to `expires` - * - `signed` sign the cookie - * - `path` defaults to "/" - * - * Examples: - * - * // "Remember Me" for 15 minutes - * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); - * - * // save as above - * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) - * - * @param {String} name - * @param {String|Object} value - * @param {Object} [options] - * @return {ServerResponse} for chaining - * @public - */ - -res.cookie = function (name, value, options) { - var opts = merge({}, options); - var secret = this.req.secret; - var signed = opts.signed; - - if (signed && !secret) { - throw new Error('cookieParser("secret") required for signed cookies'); - } - - var val = typeof value === 'object' - ? 'j:' + JSON.stringify(value) - : String(value); - - if (signed) { - val = 's:' + sign(val, secret); - } - - if ('maxAge' in opts) { - opts.expires = new Date(Date.now() + opts.maxAge); - opts.maxAge /= 1000; - } - - if (opts.path == null) { - opts.path = '/'; - } - - this.append('Set-Cookie', cookie.serialize(name, String(val), opts)); - - return this; -}; - -/** - * Set the location header to `url`. - * - * The given `url` can also be "back", which redirects - * to the _Referrer_ or _Referer_ headers or "/". - * - * Examples: - * - * res.location('/foo/bar').; - * res.location('http://example.com'); - * res.location('../login'); - * - * @param {String} url - * @return {ServerResponse} for chaining - * @public - */ - -res.location = function location(url) { - var loc = url; - - // "back" is an alias for the referrer - if (url === 'back') { - loc = this.req.get('Referrer') || '/'; - } - - // set location - return this.set('Location', encodeUrl(loc)); -}; - -/** - * Redirect to the given `url` with optional response `status` - * defaulting to 302. - * - * The resulting `url` is determined by `res.location()`, so - * it will play nicely with mounted apps, relative paths, - * `"back"` etc. - * - * Examples: - * - * res.redirect('/foo/bar'); - * res.redirect('http://example.com'); - * res.redirect(301, 'http://example.com'); - * res.redirect('../login'); // /blog/post/1 -> /blog/login - * - * @public - */ - -res.redirect = function redirect(url) { - var address = url; - var body; - var status = 302; - - // allow status / url - if (arguments.length === 2) { - status = arguments[0] - address = arguments[1] - } - - // Set location header - address = this.location(address).get('Location'); - - // Support text/{plain,html} by default - this.format({ - text: function(){ - body = statuses[status] + '. Redirecting to ' + address - }, - - html: function(){ - var u = escapeHtml(address); - body = '

' + statuses[status] + '. Redirecting to ' + u + '

' - }, - - default: function(){ - body = ''; - } - }); - - // Respond - this.statusCode = status; - this.set('Content-Length', Buffer.byteLength(body)); - - if (this.req.method === 'HEAD') { - this.end(); - } else { - this.end(body); - } -}; - -/** - * Add `field` to Vary. If already present in the Vary set, then - * this call is simply ignored. - * - * @param {Array|String} field - * @return {ServerResponse} for chaining - * @public - */ - -res.vary = function(field){ - vary(this, field); - - return this; -}; - -/** - * Render `view` with the given `options` and optional callback `fn`. - * When a callback function is given a response will _not_ be made - * automatically, otherwise a response of _200_ and _text/html_ is given. - * - * Options: - * - * - `cache` boolean hinting to the engine it should cache - * - `filename` filename of the view being rendered - * - * @public - */ - -res.render = function render(view, options, callback) { - var app = this.req.app; - var done = callback; - var opts = options || {}; - var req = this.req; - var self = this; - - // support callback function as second arg - if (typeof options === 'function') { - done = options; - opts = {}; - } - - // merge res.locals - opts._locals = self.locals; - - // default callback to respond - done = done || function (err, str) { - if (err) return req.next(err); - self.send(str); - }; - - // render - app.render(view, opts, done); -}; - -// pipe the send file stream -function sendfile(res, file, options, callback) { - var done = false; - var streaming; - - // request aborted - function onaborted() { - if (done) return; - done = true; - - var err = new Error('Request aborted'); - err.code = 'ECONNABORTED'; - callback(err); - } - - // directory - function ondirectory() { - if (done) return; - done = true; - - var err = new Error('EISDIR, read'); - err.code = 'EISDIR'; - callback(err); - } - - // errors - function onerror(err) { - if (done) return; - done = true; - callback(err); - } - - // ended - function onend() { - if (done) return; - done = true; - callback(); - } - - // file - function onfile() { - streaming = false; - } - - // finished - function onfinish(err) { - if (err && err.code === 'ECONNRESET') return onaborted(); - if (err) return onerror(err); - if (done) return; - - setImmediate(function () { - if (streaming !== false && !done) { - onaborted(); - return; - } - - if (done) return; - done = true; - callback(); - }); - } - - // streaming - function onstream() { - streaming = true; - } - - file.on('directory', ondirectory); - file.on('end', onend); - file.on('error', onerror); - file.on('file', onfile); - file.on('stream', onstream); - onFinished(res, onfinish); - - if (options.headers) { - // set headers on successful transfer - file.on('headers', function headers(res) { - var obj = options.headers; - var keys = Object.keys(obj); - - for (var i = 0; i < keys.length; i++) { - var k = keys[i]; - res.setHeader(k, obj[k]); - } - }); - } - - // pipe - file.pipe(res); -} - -/** - * Stringify JSON, like JSON.stringify, but v8 optimized, with the - * ability to escape characters that can trigger HTML sniffing. - * - * @param {*} value - * @param {function} replaces - * @param {number} spaces - * @param {boolean} escape - * @returns {string} - * @private - */ - -function stringify (value, replacer, spaces, escape) { - // v8 checks arguments.length for optimizing simple call - // https://bugs.chromium.org/p/v8/issues/detail?id=4730 - var json = replacer || spaces - ? JSON.stringify(value, replacer, spaces) - : JSON.stringify(value); - - if (escape) { - json = json.replace(/[<>&]/g, function (c) { - switch (c.charCodeAt(0)) { - case 0x3c: - return '\\u003c' - case 0x3e: - return '\\u003e' - case 0x26: - return '\\u0026' - default: - return c - } - }) - } - - return json -} +module.exports = res; diff --git a/lib/responseDecorator.js b/lib/responseDecorator.js new file mode 100644 index 0000000000..70811f8dc2 --- /dev/null +++ b/lib/responseDecorator.js @@ -0,0 +1,992 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var contentDisposition = require('content-disposition'); +var encodeUrl = require('encodeurl'); +var escapeHtml = require('escape-html'); +var onFinished = require('on-finished'); +var path = require('path'); +var pathIsAbsolute = require('path-is-absolute'); +var statuses = require('statuses') +var merge = require('utils-merge'); +var sign = require('cookie-signature').sign; +var normalizeType = require('./utils').normalizeType; +var normalizeTypes = require('./utils').normalizeTypes; +var setCharset = require('./utils').setCharset; +var cookie = require('cookie'); +var send = require('send'); +var extname = path.extname; +var mime = send.mime; +var resolve = path.resolve; +var vary = require('vary'); +/** + * Module exports. + * @public + */ + +module.exports = setMethods; + +function setMethods(res) { + /** + * Module variables. + * @private + */ + + var charsetRegExp = /;\s*charset\s*=/; + + /** + * Set status `code`. + * + * @param {Number} code + * @return {ServerResponse} + * @public + */ + + res.status = function status(code) { + this.statusCode = code; + return this; + }; + + /** + * Set Link header field with the given `links`. + * + * Examples: + * + * res.links({ + * next: 'http://api.example.com/users?page=2', + * last: 'http://api.example.com/users?page=5' + * }); + * + * @param {Object} links + * @return {ServerResponse} + * @public + */ + + res.links = function (links) { + var link = this.get('Link') || ''; + if (link) link += ', '; + return this.set('Link', link + Object.keys(links).map(function (rel) { + return '<' + links[rel] + '>; rel="' + rel + '"'; + }).join(', ')); + }; + + /** + * Send a response. + * + * Examples: + * + * res.send(Buffer.from('wahoo')); + * res.send({ some: 'json' }); + * res.send('

some html

'); + * + * @param {string|number|boolean|object|Buffer} body + * @public + */ + + res.send = function send(body) { + var chunk = body; + var encoding; + var req = this.req; + var type; + + // settings + var app = this.app; + + switch (typeof chunk) { + // string defaulting to html + case 'string': + if (!this.get('Content-Type')) { + this.type('html'); + } + break; + case 'boolean': + case 'number': + case 'object': + if (chunk === null) { + chunk = ''; + } else if (Buffer.isBuffer(chunk)) { + if (!this.get('Content-Type')) { + this.type('bin'); + } + } else { + return this.json(chunk); + } + break; + } + + // write strings in utf-8 + if (typeof chunk === 'string') { + encoding = 'utf8'; + type = this.get('Content-Type'); + + // reflect this in content-type + if (typeof type === 'string') { + this.set('Content-Type', setCharset(type, 'utf-8')); + } + } + + // determine if ETag should be generated + var etagFn = app.get('etag fn') + var generateETag = !this.get('ETag') && typeof etagFn === 'function' + + // populate Content-Length + var len + if (chunk !== undefined) { + if (Buffer.isBuffer(chunk)) { + // get length of Buffer + len = chunk.length + } else if (!generateETag && chunk.length < 1000) { + // just calculate length when no ETag + small chunk + len = Buffer.byteLength(chunk, encoding) + } else { + // convert chunk to Buffer and calculate + chunk = Buffer.from(chunk, encoding) + encoding = undefined; + len = chunk.length + } + + this.set('Content-Length', len); + } + + // populate ETag + var etag; + if (generateETag && len !== undefined) { + if ((etag = etagFn(chunk, encoding))) { + this.set('ETag', etag); + } + } + + // freshness + if (req.fresh) this.statusCode = 304; + + // strip irrelevant headers + if (204 === this.statusCode || 304 === this.statusCode) { + this.removeHeader('Content-Type'); + this.removeHeader('Content-Length'); + this.removeHeader('Transfer-Encoding'); + chunk = ''; + } + + if (req.method === 'HEAD') { + // skip body for HEAD + this.end(); + } else { + // respond + this.end(chunk, encoding); + } + + return this; + }; + + /** + * Send JSON response. + * + * Examples: + * + * res.json(null); + * res.json({ user: 'tj' }); + * + * @param {string|number|boolean|object} obj + * @public + */ + + res.json = function json(obj) { + // settings + var app = this.app; + var escape = app.get('json escape') + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = stringify(obj, replacer, spaces, escape) + + // content-type + if (!this.get('Content-Type')) { + this.set('Content-Type', 'application/json'); + } + + return this.send(body); + }; + + /** + * Send JSON response with JSONP callback support. + * + * Examples: + * + * res.jsonp(null); + * res.jsonp({ user: 'tj' }); + * + * @param {string|number|boolean|object} obj + * @public + */ + + res.jsonp = function jsonp(obj) { + // settings + var app = this.app; + var escape = app.get('json escape') + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = stringify(obj, replacer, spaces, escape) + var callback = this.req.query[app.get('jsonp callback name')]; + + // content-type + if (!this.get('Content-Type')) { + this.set('X-Content-Type-Options', 'nosniff'); + this.set('Content-Type', 'application/json'); + } + + // fixup callback + if (Array.isArray(callback)) { + callback = callback[0]; + } + + // jsonp + if (typeof callback === 'string' && callback.length !== 0) { + this.set('X-Content-Type-Options', 'nosniff'); + this.set('Content-Type', 'text/javascript'); + + // restrict callback charset + callback = callback.replace(/[^\[\]\w$.]/g, ''); + + // replace chars not allowed in JavaScript that are in JSON + body = body + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); + + // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" + // the typeof check is just to reduce client error noise + body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');'; + } + + return this.send(body); + }; + + /** + * Send given HTTP status code. + * + * Sets the response status to `statusCode` and the body of the + * response to the standard description from node's http.STATUS_CODES + * or the statusCode number if no description. + * + * Examples: + * + * res.sendStatus(200); + * + * @param {number} statusCode + * @public + */ + + res.sendStatus = function sendStatus(statusCode) { + var body = statuses[statusCode] || String(statusCode) + + this.statusCode = statusCode; + this.type('txt'); + + return this.send(body); + }; + + /** + * Transfer the file at the given `path`. + * + * Automatically sets the _Content-Type_ response header field. + * The callback `callback(err)` is invoked when the transfer is complete + * or when an error occurs. Be sure to check `res.sentHeader` + * if you wish to attempt responding, as the header and some data + * may have already been transferred. + * + * Options: + * + * - `maxAge` defaulting to 0 (can be string converted by `ms`) + * - `root` root directory for relative filenames + * - `headers` object of headers to serve with file + * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them + * + * Other options are passed along to `send`. + * + * Examples: + * + * The following example illustrates how `res.sendFile()` may + * be used as an alternative for the `static()` middleware for + * dynamic situations. The code backing `res.sendFile()` is actually + * the same code, so HTTP cache support etc is identical. + * + * app.get('/user/:uid/photos/:file', function(req, res){ + * var uid = req.params.uid + * , file = req.params.file; + * + * req.user.mayViewFilesFrom(uid, function(yes){ + * if (yes) { + * res.sendFile('/uploads/' + uid + '/' + file); + * } else { + * res.send(403, 'Sorry! you cant see that.'); + * } + * }); + * }); + * + * @public + */ + + res.sendFile = function sendFile(path, options, callback) { + var done = callback; + var req = this.req; + var res = this; + var next = req.next; + var opts = options || {}; + + if (!path) { + throw new TypeError('path argument is required to res.sendFile'); + } + + // support function as second arg + if (typeof options === 'function') { + done = options; + opts = {}; + } + + if (!opts.root && !pathIsAbsolute(path)) { + throw new TypeError('path must be absolute or specify root to res.sendFile'); + } + + // create file stream + var pathname = encodeURI(path); + var file = send(req, pathname, opts); + + // transfer + sendfile(res, file, opts, function (err) { + if (done) return done(err); + if (err && err.code === 'EISDIR') return next(); + + // next() all but write errors + if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') { + next(err); + } + }); + }; + + /** + * Transfer the file at the given `path` as an attachment. + * + * Optionally providing an alternate attachment `filename`, + * and optional callback `callback(err)`. The callback is invoked + * when the data transfer is complete, or when an error has + * ocurred. Be sure to check `res.headersSent` if you plan to respond. + * + * Optionally providing an `options` object to use with `res.sendFile()`. + * This function will set the `Content-Disposition` header, overriding + * any `Content-Disposition` header passed as header options in order + * to set the attachment and filename. + * + * This method uses `res.sendFile()`. + * + * @public + */ + + res.download = function download (path, filename, options, callback) { + var done = callback; + var name = filename; + var opts = options || null + + // support function as second or third arg + if (typeof filename === 'function') { + done = filename; + name = null; + opts = null + } else if (typeof options === 'function') { + done = options + opts = null + } + + // set Content-Disposition when file is sent + var headers = { + 'Content-Disposition': contentDisposition(name || path) + }; + + // merge user-provided headers + if (opts && opts.headers) { + var keys = Object.keys(opts.headers) + for (var i = 0; i < keys.length; i++) { + var key = keys[i] + if (key.toLowerCase() !== 'content-disposition') { + headers[key] = opts.headers[key] + } + } + } + + // merge user-provided options + opts = Object.create(opts) + opts.headers = headers + + // Resolve the full path for sendFile + var fullPath = resolve(path); + + // send file + return this.sendFile(fullPath, opts, done) + }; + + /** + * Set _Content-Type_ response header with `type` through `mime.lookup()` + * when it does not contain "/", or set the Content-Type to `type` otherwise. + * + * Examples: + * + * res.type('.html'); + * res.type('html'); + * res.type('json'); + * res.type('application/json'); + * res.type('png'); + * + * @param {String} type + * @return {ServerResponse} for chaining + * @public + */ + + res.contentType = + res.type = function contentType(type) { + var ct = type.indexOf('/') === -1 + ? mime.lookup(type) + : type; + + return this.set('Content-Type', ct); + }; + + /** + * Respond to the Acceptable formats using an `obj` + * of mime-type callbacks. + * + * This method uses `req.accepted`, an array of + * acceptable types ordered by their quality values. + * When "Accept" is not present the _first_ callback + * is invoked, otherwise the first match is used. When + * no match is performed the server responds with + * 406 "Not Acceptable". + * + * Content-Type is set for you, however if you choose + * you may alter this within the callback using `res.type()` + * or `res.set('Content-Type', ...)`. + * + * res.format({ + * 'text/plain': function(){ + * res.send('hey'); + * }, + * + * 'text/html': function(){ + * res.send('

hey

'); + * }, + * + * 'appliation/json': function(){ + * res.send({ message: 'hey' }); + * } + * }); + * + * In addition to canonicalized MIME types you may + * also use extnames mapped to these types: + * + * res.format({ + * text: function(){ + * res.send('hey'); + * }, + * + * html: function(){ + * res.send('

hey

'); + * }, + * + * json: function(){ + * res.send({ message: 'hey' }); + * } + * }); + * + * By default Express passes an `Error` + * with a `.status` of 406 to `next(err)` + * if a match is not made. If you provide + * a `.default` callback it will be invoked + * instead. + * + * @param {Object} obj + * @return {ServerResponse} for chaining + * @public + */ + + res.format = function (obj) { + var req = this.req; + var next = req.next; + + var fn = obj.default; + if (fn) delete obj.default; + var keys = Object.keys(obj); + + var key = keys.length > 0 + ? req.accepts(keys) + : false; + + this.vary("Accept"); + + if (key) { + this.set('Content-Type', normalizeType(key).value); + obj[key](req, this, next); + } else if (fn) { + fn(); + } else { + var err = new Error('Not Acceptable'); + err.status = err.statusCode = 406; + err.types = normalizeTypes(keys).map(function (o) { return o.value }); + next(err); + } + + return this; + }; + + /** + * Set _Content-Disposition_ header to _attachment_ with optional `filename`. + * + * @param {String} filename + * @return {ServerResponse} + * @public + */ + + res.attachment = function attachment(filename) { + if (filename) { + this.type(extname(filename)); + } + + this.set('Content-Disposition', contentDisposition(filename)); + + return this; + }; + + /** + * Append additional header `field` with value `val`. + * + * Example: + * + * res.append('Link', ['', '']); + * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); + * res.append('Warning', '199 Miscellaneous warning'); + * + * @param {String} field + * @param {String|Array} val + * @return {ServerResponse} for chaining + * @public + */ + + res.append = function append(field, val) { + var prev = this.get(field); + var value = val; + + if (prev) { + // concat the new and prev vals + value = Array.isArray(prev) ? prev.concat(val) + : Array.isArray(val) ? [prev].concat(val) + : [prev, val]; + } + + return this.set(field, value); + }; + + /** + * Set header `field` to `val`, or pass + * an object of header fields. + * + * Examples: + * + * res.set('Foo', ['bar', 'baz']); + * res.set('Accept', 'application/json'); + * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); + * + * Aliased as `res.header()`. + * + * @param {String|Object} field + * @param {String|Array} val + * @return {ServerResponse} for chaining + * @public + */ + + res.set = + res.header = function header(field, val) { + if (arguments.length === 2) { + var value = Array.isArray(val) + ? val.map(String) + : String(val); + + // add charset to content-type + if (field.toLowerCase() === 'content-type') { + if (Array.isArray(value)) { + throw new TypeError('Content-Type cannot be set to an Array'); + } + if (!charsetRegExp.test(value)) { + var charset = mime.charsets.lookup(value.split(';')[0]); + if (charset) value += '; charset=' + charset.toLowerCase(); + } + } + + this.setHeader(field, value); + } else { + for (var key in field) { + this.set(key, field[key]); + } + } + return this; + }; + + /** + * Get value for header `field`. + * + * @param {String} field + * @return {String} + * @public + */ + + res.get = function (field) { + return this.getHeader(field); + }; + + /** + * Clear cookie `name`. + * + * @param {String} name + * @param {Object} [options] + * @return {ServerResponse} for chaining + * @public + */ + + res.clearCookie = function clearCookie(name, options) { + var opts = merge({ expires: new Date(1), path: '/' }, options); + + return this.cookie(name, '', opts); + }; + + /** + * Set cookie `name` to `value`, with the given `options`. + * + * Options: + * + * - `maxAge` max-age in milliseconds, converted to `expires` + * - `signed` sign the cookie + * - `path` defaults to "/" + * + * Examples: + * + * // "Remember Me" for 15 minutes + * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); + * + * // save as above + * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) + * + * @param {String} name + * @param {String|Object} value + * @param {Object} [options] + * @return {ServerResponse} for chaining + * @public + */ + + res.cookie = function (name, value, options) { + var opts = merge({}, options); + var secret = this.req.secret; + var signed = opts.signed; + + if (signed && !secret) { + throw new Error('cookieParser("secret") required for signed cookies'); + } + + var val = typeof value === 'object' + ? 'j:' + JSON.stringify(value) + : String(value); + + if (signed) { + val = 's:' + sign(val, secret); + } + + if ('maxAge' in opts) { + opts.expires = new Date(Date.now() + opts.maxAge); + opts.maxAge /= 1000; + } + + if (opts.path == null) { + opts.path = '/'; + } + + this.append('Set-Cookie', cookie.serialize(name, String(val), opts)); + + return this; + }; + + /** + * Set the location header to `url`. + * + * The given `url` can also be "back", which redirects + * to the _Referrer_ or _Referer_ headers or "/". + * + * Examples: + * + * res.location('/foo/bar').; + * res.location('http://example.com'); + * res.location('../login'); + * + * @param {String} url + * @return {ServerResponse} for chaining + * @public + */ + + res.location = function location(url) { + var loc = url; + + // "back" is an alias for the referrer + if (url === 'back') { + loc = this.req.get('Referrer') || '/'; + } + + // set location + return this.set('Location', encodeUrl(loc)); + }; + + /** + * Redirect to the given `url` with optional response `status` + * defaulting to 302. + * + * The resulting `url` is determined by `res.location()`, so + * it will play nicely with mounted apps, relative paths, + * `"back"` etc. + * + * Examples: + * + * res.redirect('/foo/bar'); + * res.redirect('http://example.com'); + * res.redirect(301, 'http://example.com'); + * res.redirect('../login'); // /blog/post/1 -> /blog/login + * + * @public + */ + + res.redirect = function redirect(url) { + var address = url; + var body; + var status = 302; + + // allow status / url + if (arguments.length === 2) { + status = arguments[0] + address = arguments[1] + } + + // Set location header + address = this.location(address).get('Location'); + + // Support text/{plain,html} by default + this.format({ + text: function () { + body = statuses[status] + '. Redirecting to ' + address + }, + + html: function () { + var u = escapeHtml(address); + body = '

' + statuses[status] + '. Redirecting to ' + u + '

' + }, + + default: function () { + body = ''; + } + }); + + // Respond + this.statusCode = status; + this.set('Content-Length', Buffer.byteLength(body)); + + if (this.req.method === 'HEAD') { + this.end(); + } else { + this.end(body); + } + }; + + /** + * Add `field` to Vary. If already present in the Vary set, then + * this call is simply ignored. + * + * @param {Array|String} field + * @return {ServerResponse} for chaining + * @public + */ + + res.vary = function(field){ + vary(this, field); + + return this; + }; + + /** + * Render `view` with the given `options` and optional callback `fn`. + * When a callback function is given a response will _not_ be made + * automatically, otherwise a response of _200_ and _text/html_ is given. + * + * Options: + * + * - `cache` boolean hinting to the engine it should cache + * - `filename` filename of the view being rendered + * + * @public + */ + + res.render = function render(view, options, callback) { + var app = this.req.app; + var done = callback; + var opts = options || {}; + var req = this.req; + var self = this; + + // support callback function as second arg + if (typeof options === 'function') { + done = options; + opts = {}; + } + + // merge res.locals + opts._locals = self.locals; + + // default callback to respond + done = done || function (err, str) { + if (err) return req.next(err); + self.send(str); + }; + + // render + app.render(view, opts, done); + }; + + // pipe the send file stream + function sendfile(res, file, options, callback) { + var done = false; + var streaming; + + // request aborted + function onaborted() { + if (done) return; + done = true; + + var err = new Error('Request aborted'); + err.code = 'ECONNABORTED'; + callback(err); + } + + // directory + function ondirectory() { + if (done) return; + done = true; + + var err = new Error('EISDIR, read'); + err.code = 'EISDIR'; + callback(err); + } + + // errors + function onerror(err) { + if (done) return; + done = true; + callback(err); + } + + // ended + function onend() { + if (done) return; + done = true; + callback(); + } + + // file + function onfile() { + streaming = false; + } + + // finished + function onfinish(err) { + if (err && err.code === 'ECONNRESET') return onaborted(); + if (err) return onerror(err); + if (done) return; + + setImmediate(function () { + if (streaming !== false && !done) { + onaborted(); + return; + } + + if (done) return; + done = true; + callback(); + }); + } + + // streaming + function onstream() { + streaming = true; + } + + file.on('directory', ondirectory); + file.on('end', onend); + file.on('error', onerror); + file.on('file', onfile); + file.on('stream', onstream); + onFinished(res, onfinish); + + if (options.headers) { + // set headers on successful transfer + file.on('headers', function headers(res) { + var obj = options.headers; + var keys = Object.keys(obj); + + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + res.setHeader(k, obj[k]); + } + }); + } + + // pipe + file.pipe(res); + } + + return res; +} + +/** + * Stringify JSON, like JSON.stringify, but v8 optimized, with the + * ability to escape characters that can trigger HTML sniffing. + * + * @param {*} value + * @param {function} replaces + * @param {number} spaces + * @param {boolean} escape + * @returns {string} + * @private + */ + +function stringify (value, replacer, spaces, escape) { + // v8 checks arguments.length for optimizing simple call + // https://bugs.chromium.org/p/v8/issues/detail?id=4730 + var json = replacer || spaces + ? JSON.stringify(value, replacer, spaces) + : JSON.stringify(value); + + if (escape) { + json = json.replace(/[<>&]/g, function (c) { + switch (c.charCodeAt(0)) { + case 0x3c: + return '\\u003c' + case 0x3e: + return '\\u003e' + case 0x26: + return '\\u0026' + default: + return c + } + }) + } + + return json +} diff --git a/lib/utils.js b/lib/utils.js index 7e86a43a16..afb5bd5f6f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -19,7 +19,17 @@ var etag = require('etag'); var proxyaddr = require('proxy-addr'); var qs = require('qs'); var querystring = require('querystring'); +var isHttp2Supported = true; +/** + * Test for http2 support + * @api private + */ +try { + require('http2'); +} catch (_) { + isHttp2Supported = false; +} /** * Return strong ETag for `body`. * @@ -197,6 +207,10 @@ exports.compileTrust = function(val) { return proxyaddr.compile(val || []); } +/** + * Flag for http2 support + */ +exports.isHttp2Supported = isHttp2Supported; /** * Set the charset in a given Content-Type string. * From 1e596eee79ce3225a7811b2d15eccfc08ab9fe77 Mon Sep 17 00:00:00 2001 From: sogaani Date: Sat, 28 Jul 2018 10:27:05 +0900 Subject: [PATCH 2/6] Enable http2 tests --- .eslintignore | 1 + .travis.yml | 4 + examples/mvc/index.js | 3 + lib/application.js | 13 +- lib/express.js | 14 +- lib/http2Request.js | 21 +++ lib/requestDecorator.js | 17 +-- lib/responseDecorator.js | 43 +++--- package.json | 3 +- test/acceptance/auth.js | 2 +- test/acceptance/content-negotiation.js | 2 +- test/acceptance/cookie-sessions.js | 34 +++-- test/acceptance/cookies.js | 2 +- test/acceptance/downloads.js | 2 +- test/acceptance/ejs.js | 2 +- test/acceptance/error-pages.js | 2 +- test/acceptance/error.js | 2 +- test/acceptance/markdown.js | 2 +- test/acceptance/multi-router.js | 2 +- test/acceptance/mvc.js | 2 +- test/acceptance/params.js | 2 +- test/acceptance/resource.js | 2 +- test/acceptance/route-map.js | 2 +- test/acceptance/route-separation.js | 2 +- test/acceptance/vhost.js | 70 +++++----- test/acceptance/web-service.js | 2 +- test/app.all.js | 2 +- test/app.head.js | 2 +- test/app.js | 2 +- test/app.options.js | 2 +- test/app.param.js | 2 +- test/app.request.js | 5 +- test/app.response.js | 11 +- test/app.route.js | 2 +- test/app.router.js | 4 +- test/app.routes.error.js | 2 +- test/app.use.js | 2 +- test/exports.js | 8 +- test/middleware.basic.js | 4 +- test/regression.js | 2 +- test/req.accepts.js | 2 +- test/req.acceptsCharsets.js | 2 +- test/req.acceptsEncodings.js | 2 +- test/req.acceptsLanguages.js | 2 +- test/req.baseUrl.js | 2 +- test/req.fresh.js | 2 +- test/req.get.js | 2 +- test/req.host.js | 3 +- test/req.hostname.js | 3 +- test/req.ip.js | 2 +- test/req.ips.js | 2 +- test/req.is.js | 2 +- test/req.path.js | 2 +- test/req.protocol.js | 2 +- test/req.query.js | 2 +- test/req.range.js | 2 +- test/req.route.js | 2 +- test/req.secure.js | 2 +- test/req.signedCookies.js | 2 +- test/req.stale.js | 2 +- test/req.subdomains.js | 3 +- test/req.xhr.js | 2 +- test/res.append.js | 2 +- test/res.attachment.js | 2 +- test/res.clearCookie.js | 2 +- test/res.cookie.js | 2 +- test/res.download.js | 2 +- test/res.format.js | 2 +- test/res.get.js | 2 +- test/res.json.js | 2 +- test/res.jsonp.js | 2 +- test/res.links.js | 2 +- test/res.locals.js | 2 +- test/res.location.js | 2 +- test/res.redirect.js | 4 +- test/res.render.js | 2 +- test/res.send.js | 4 +- test/res.sendFile.js | 40 +++--- test/res.sendStatus.js | 2 +- test/res.set.js | 2 +- test/res.status.js | 2 +- test/res.type.js | 2 +- test/res.vary.js | 2 +- test/support/http2wrapper.js | 186 +++++++++++++++++++++++++ test/support/supertest.js | 33 +++++ 85 files changed, 469 insertions(+), 184 deletions(-) create mode 100644 test/support/http2wrapper.js create mode 100644 test/support/supertest.js diff --git a/.eslintignore b/.eslintignore index 62562b74a3..14a092a7d4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ coverage node_modules +test/support/http2wrapper.js diff --git a/.travis.yml b/.travis.yml index 855168ff54..ebc301bba6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,12 @@ matrix: include: - node_js: "8" env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" + - node_js: "8" + env: HTTP2_TEST=1 - node_js: "9" env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" + - node_js: "9" + env: HTTP2_TEST=1 allow_failures: # Allow the nightly installs to fail - env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" diff --git a/examples/mvc/index.js b/examples/mvc/index.js index 77885a60ca..db9c104bf9 100644 --- a/examples/mvc/index.js +++ b/examples/mvc/index.js @@ -27,6 +27,9 @@ app.response.message = function(msg){ sess.messages.push(msg); return this; }; +if(app.isHttp2Supported){ + app.http2Response.message = app.response.message; +} // log if (!module.parent) app.use(logger('dev')); diff --git a/lib/application.js b/lib/application.js index b87d8f65ae..f3409a9f3c 100644 --- a/lib/application.js +++ b/lib/application.js @@ -19,7 +19,7 @@ var debug = require('debug')('express:application'); var View = require('./view'); var http = require('http'); var compileETag = require('./utils').compileETag; -var isHttp2Suported = require('./utils').isHttp2Supported; +var isHttp2Supported = require('./utils').isHttp2Supported; var compileQueryParser = require('./utils').compileQueryParser; var compileTrust = require('./utils').compileTrust; var flatten = require('array-flatten'); @@ -28,7 +28,11 @@ var resolve = require('path').resolve; var Router = require('router'); var setPrototypeOf = require('setprototypeof') var slice = Array.prototype.slice; +var http2Request = null; +if (isHttp2Supported) { + http2Request = require('http2').Http2ServerRequest; +} /** * Application prototype. */ @@ -116,7 +120,7 @@ app.defaultConfiguration = function defaultConfiguration() { setPrototypeOf(this.engines, parent.engines) setPrototypeOf(this.settings, parent.settings) // set prototype for http2 requests/response - if (isHttp2Suported) { + if (isHttp2Supported) { setPrototypeOf(this.http2Request, parent.http2Request) setPrototypeOf(this.http2Response, parent.http2Response) } @@ -136,6 +140,9 @@ app.defaultConfiguration = function defaultConfiguration() { this.set('views', resolve('views')); this.set('jsonp callback name', 'callback'); + // http2 support + this.isHttp2Supported = isHttp2Supported; + if (env === 'production') { this.enable('view cache'); } @@ -236,7 +243,7 @@ app.use = function use(fn) { router.use(path, function mounted_app(req, res, next) { var orig = req.app; fn.handle(req, res, function (err) { - if (typeof orig.http2Request !== 'undefined' && req instanceof http2Request) { + if (orig.isHttp2Supported && req instanceof http2Request) { setPrototypeOf(req, orig.http2Request) setPrototypeOf(res, orig.http2Response) } else { diff --git a/lib/express.js b/lib/express.js index 25035aaf21..b4209105af 100644 --- a/lib/express.js +++ b/lib/express.js @@ -20,6 +20,12 @@ var Router = require('router'); var req = require('./request'); var res = require('./response'); var isHttp2Supported = require('./utils').isHttp2Supported; +var http2Req = null; +var http2Res = null; +if (isHttp2Supported) { + http2Req = require('./http2Request'); + http2Res = require('./http2Response'); +} /** * Expose `createApplication()`. */ @@ -52,8 +58,6 @@ function createApplication() { }) if (isHttp2Supported) { - var http2Req = require('./http2Request'); - var http2Res = require('./http2Response'); app.http2Request = Object.create(http2Req, { app: { configurable: true, enumerable: true, writable: true, value: app } }); @@ -63,9 +67,6 @@ function createApplication() { }); } - - - app.init(); return app; } @@ -77,6 +78,9 @@ function createApplication() { exports.application = proto; exports.request = req; exports.response = res; +exports.http2Request = http2Req; +exports.http2Response = http2Res; +exports.isHttp2Supported = isHttp2Supported; /** * Expose constructors. diff --git a/lib/http2Request.js b/lib/http2Request.js index 733c095ba7..884f7fdfb0 100644 --- a/lib/http2Request.js +++ b/lib/http2Request.js @@ -13,8 +13,29 @@ * @private */ var http2 = require('http2'); +var HTTP2_HEADER_AUTHORITY = http2.constants.HTTP2_HEADER_AUTHORITY; var decorator = require('./requestDecorator'); var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); + +/** + * req.host and req.hostname refer to ':authority' haeder for compatibility. + */ + +Object.defineProperty(http2Req, 'host', { + configurable: true, + enumerable: true, + get: function host() { + var trust = this.app.get('trust proxy fn'); + var val = this.get('X-Forwarded-Host'); + + if (!val || !trust(this.connection.remoteAddress, 0)) { + val = this.get(HTTP2_HEADER_AUTHORITY); + } + + return val || undefined; + } +}); + /** * Module exports. * @public diff --git a/lib/requestDecorator.js b/lib/requestDecorator.js index 996613261c..540ca10a20 100644 --- a/lib/requestDecorator.js +++ b/lib/requestDecorator.js @@ -14,7 +14,6 @@ */ var accepts = require('accepts'); -var deprecate = require('depd')('express'); var isIP = require('net').isIP; var typeis = require('type-is'); var fresh = require('fresh'); @@ -146,9 +145,6 @@ function setMethods(req) { return accept.encodings.apply(accept, arguments); }; - req.acceptsEncoding = deprecate.function(req.acceptsEncodings, - 'req.acceptsEncoding: Use acceptsEncodings instead'); - /** * Check if the given `charset`s are acceptable, * otherwise you should respond with 406 "Not Acceptable". @@ -299,7 +295,7 @@ function setMethods(req) { // single value, but this is to be safe. var header = this.get('X-Forwarded-Proto') || proto var index = header.indexOf(',') - + return index !== -1 ? header.substring(0, index).trim() : header.trim() @@ -386,7 +382,7 @@ function setMethods(req) { /** * Short-hand for `url.parse(req.url).pathname`. - * + * * @return {String} * @public */ @@ -413,7 +409,7 @@ function setMethods(req) { if (!val || !trust(this.connection.remoteAddress, 0)) { val = this.get('Host'); } - + return val || undefined; }); @@ -430,15 +426,15 @@ function setMethods(req) { defineGetter(req, 'hostname', function hostname(){ var host = this.host; - + if (!host) return; - + // IPv6 literal support var offset = host[0] === '[' ? host.indexOf(']') + 1 : 0; var index = host.indexOf(':', offset); - + return index !== -1 ? host.substring(0, index) : host; @@ -508,7 +504,6 @@ function setMethods(req) { * @param {Function} getter * @private */ - function defineGetter(obj, name, getter) { Object.defineProperty(obj, name, { configurable: true, diff --git a/lib/responseDecorator.js b/lib/responseDecorator.js index 70811f8dc2..2469c14ee7 100644 --- a/lib/responseDecorator.js +++ b/lib/responseDecorator.js @@ -12,6 +12,7 @@ * @private */ +var Buffer = require('safe-buffer').Buffer var contentDisposition = require('content-disposition'); var encodeUrl = require('encodeurl'); var escapeHtml = require('escape-html'); @@ -99,10 +100,10 @@ function setMethods(res) { var encoding; var req = this.req; var type; - + // settings var app = this.app; - + switch (typeof chunk) { // string defaulting to html case 'string': @@ -124,22 +125,22 @@ function setMethods(res) { } break; } - + // write strings in utf-8 if (typeof chunk === 'string') { encoding = 'utf8'; type = this.get('Content-Type'); - + // reflect this in content-type if (typeof type === 'string') { this.set('Content-Type', setCharset(type, 'utf-8')); } } - + // determine if ETag should be generated var etagFn = app.get('etag fn') var generateETag = !this.get('ETag') && typeof etagFn === 'function' - + // populate Content-Length var len if (chunk !== undefined) { @@ -155,10 +156,10 @@ function setMethods(res) { encoding = undefined; len = chunk.length } - + this.set('Content-Length', len); } - + // populate ETag var etag; if (generateETag && len !== undefined) { @@ -166,10 +167,10 @@ function setMethods(res) { this.set('ETag', etag); } } - + // freshness if (req.fresh) this.statusCode = 304; - + // strip irrelevant headers if (204 === this.statusCode || 304 === this.statusCode) { this.removeHeader('Content-Type'); @@ -177,7 +178,7 @@ function setMethods(res) { this.removeHeader('Transfer-Encoding'); chunk = ''; } - + if (req.method === 'HEAD') { // skip body for HEAD this.end(); @@ -185,7 +186,7 @@ function setMethods(res) { // respond this.end(chunk, encoding); } - + return this; }; @@ -394,7 +395,7 @@ function setMethods(res) { var done = callback; var name = filename; var opts = options || null - + // support function as second or third arg if (typeof filename === 'function') { done = filename; @@ -404,12 +405,12 @@ function setMethods(res) { done = options opts = null } - + // set Content-Disposition when file is sent var headers = { 'Content-Disposition': contentDisposition(name || path) }; - + // merge user-provided headers if (opts && opts.headers) { var keys = Object.keys(opts.headers) @@ -420,14 +421,14 @@ function setMethods(res) { } } } - + // merge user-provided options opts = Object.create(opts) opts.headers = headers - + // Resolve the full path for sendFile var fullPath = resolve(path); - + // send file return this.sendFile(fullPath, opts, done) }; @@ -774,7 +775,7 @@ function setMethods(res) { if (arguments.length === 2) { status = arguments[0] address = arguments[1] - } + } // Set location header address = this.location(address).get('Location'); @@ -817,10 +818,10 @@ function setMethods(res) { res.vary = function(field){ vary(this, field); - + return this; }; - + /** * Render `view` with the given `options` and optional callback `fn`. * When a callback function is given a response will _not_ be made diff --git a/package.json b/package.json index 365b1d46c1..c27324006d 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "multiparty": "4.1.3", "pbkdf2-password": "1.2.1", "should": "13.1.0", - "supertest": "1.2.0", + "supertest": "2.0", "connect-redis": "~2.4.1", "vhost": "~3.0.2" }, @@ -92,6 +92,7 @@ "scripts": { "lint": "eslint .", "test": "mocha --require test/support/env --reporter spec --bail --check-leaks --no-exit test/ test/acceptance/", + "test-http2": "HTTP2_TEST=1 mocha --require test/support/env --reporter spec --bail --check-leaks --no-exit test/ test/acceptance/", "test-ci": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks --no-exit test/ test/acceptance/", "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks --no-exit test/ test/acceptance/", "test-tap": "mocha --require test/support/env --reporter tap --check-leaks --no-exit test/ test/acceptance/" diff --git a/test/acceptance/auth.js b/test/acceptance/auth.js index 9a36ea45fe..e1699d8be5 100644 --- a/test/acceptance/auth.js +++ b/test/acceptance/auth.js @@ -1,5 +1,5 @@ var app = require('../../examples/auth') -var request = require('supertest') +var request = require('../support/supertest') function getCookie(res) { return res.headers['set-cookie'][0].split(';')[0]; diff --git a/test/acceptance/content-negotiation.js b/test/acceptance/content-negotiation.js index ac9dbaf557..72104206b1 100644 --- a/test/acceptance/content-negotiation.js +++ b/test/acceptance/content-negotiation.js @@ -1,5 +1,5 @@ -var request = require('supertest') +var request = require('../support/supertest') , app = require('../../examples/content-negotiation'); describe('content-negotiation', function(){ diff --git a/test/acceptance/cookie-sessions.js b/test/acceptance/cookie-sessions.js index d438cfe6d5..865ca817fe 100644 --- a/test/acceptance/cookie-sessions.js +++ b/test/acceptance/cookie-sessions.js @@ -1,6 +1,6 @@ var app = require('../../examples/cookie-sessions') -var request = require('supertest') +var request = require('../support/supertest') describe('cookie-sessions', function () { describe('GET /', function () { @@ -10,24 +10,28 @@ describe('cookie-sessions', function () { .expect(200, 'viewed 1 times\n', done) }) - it('should set a session cookie', function (done) { - request(app) - .get('/') - .expect('Set-Cookie', /express:sess=/) - .expect(200, done) - }) + // cookies with http2 has an issue. + // See also https://github.com/pillarjs/cookies/pull/99 + if (!process.env.HTTP2_TEST) { + it('should set a session cookie', function (done) { + request(app) + .get('/') + .expect('Set-Cookie', /express:sess=/) + .expect(200, done) + }) - it('should display 1 view on revisit', function (done) { - request(app) - .get('/') - .expect(200, 'viewed 1 times\n', function (err, res) { - if (err) return done(err) + it('should display 1 view on revisit', function (done) { request(app) .get('/') - .set('Cookie', getCookies(res)) - .expect(200, 'viewed 2 times\n', done) + .expect(200, 'viewed 1 times\n', function (err, res) { + if (err) return done(err) + request(app) + .get('/') + .set('Cookie', getCookies(res)) + .expect(200, 'viewed 2 times\n', done) + }) }) - }) + } }) }) diff --git a/test/acceptance/cookies.js b/test/acceptance/cookies.js index aa9e1faef4..604e24996b 100644 --- a/test/acceptance/cookies.js +++ b/test/acceptance/cookies.js @@ -1,6 +1,6 @@ var app = require('../../examples/cookies') - , request = require('supertest'); + , request = require('../support/supertest'); var utils = require('../support/utils'); describe('cookies', function(){ diff --git a/test/acceptance/downloads.js b/test/acceptance/downloads.js index a0aa7b75d9..9e79ed2333 100644 --- a/test/acceptance/downloads.js +++ b/test/acceptance/downloads.js @@ -1,6 +1,6 @@ var app = require('../../examples/downloads') - , request = require('supertest'); + , request = require('../support/supertest'); describe('downloads', function(){ describe('GET /', function(){ diff --git a/test/acceptance/ejs.js b/test/acceptance/ejs.js index 12defcb564..32344032f1 100644 --- a/test/acceptance/ejs.js +++ b/test/acceptance/ejs.js @@ -1,5 +1,5 @@ -var request = require('supertest') +var request = require('../support/supertest') , app = require('../../examples/ejs'); describe('ejs', function(){ diff --git a/test/acceptance/error-pages.js b/test/acceptance/error-pages.js index 9af950178d..2a33d26f8e 100644 --- a/test/acceptance/error-pages.js +++ b/test/acceptance/error-pages.js @@ -1,6 +1,6 @@ var app = require('../../examples/error-pages') - , request = require('supertest'); + , request = require('../support/supertest'); describe('error-pages', function(){ describe('GET /', function(){ diff --git a/test/acceptance/error.js b/test/acceptance/error.js index 6bdf099fee..7bdd2eae00 100644 --- a/test/acceptance/error.js +++ b/test/acceptance/error.js @@ -1,6 +1,6 @@ var app = require('../../examples/error') - , request = require('supertest'); + , request = require('../support/supertest'); describe('error', function(){ describe('GET /', function(){ diff --git a/test/acceptance/markdown.js b/test/acceptance/markdown.js index 1a7d9e3cb7..a08fc889b0 100644 --- a/test/acceptance/markdown.js +++ b/test/acceptance/markdown.js @@ -1,6 +1,6 @@ var app = require('../../examples/markdown') -var request = require('supertest') +var request = require('../support/supertest') describe('markdown', function(){ describe('GET /', function(){ diff --git a/test/acceptance/multi-router.js b/test/acceptance/multi-router.js index 9590ee94d4..ca21ad7e85 100644 --- a/test/acceptance/multi-router.js +++ b/test/acceptance/multi-router.js @@ -1,5 +1,5 @@ var app = require('../../examples/multi-router') -var request = require('supertest') +var request = require('../support/supertest') describe('multi-router', function(){ describe('GET /',function(){ diff --git a/test/acceptance/mvc.js b/test/acceptance/mvc.js index 35709f6fb4..a4a767ed2b 100644 --- a/test/acceptance/mvc.js +++ b/test/acceptance/mvc.js @@ -1,5 +1,5 @@ -var request = require('supertest') +var request = require('../support/supertest') , app = require('../../examples/mvc'); describe('mvc', function(){ diff --git a/test/acceptance/params.js b/test/acceptance/params.js index e7c30cf773..ad202717d0 100644 --- a/test/acceptance/params.js +++ b/test/acceptance/params.js @@ -1,5 +1,5 @@ var app = require('../../examples/params') -var request = require('supertest') +var request = require('../support/supertest') describe('params', function(){ describe('GET /', function(){ diff --git a/test/acceptance/resource.js b/test/acceptance/resource.js index a2db7bd3fd..9c3b3c9b36 100644 --- a/test/acceptance/resource.js +++ b/test/acceptance/resource.js @@ -1,5 +1,5 @@ var app = require('../../examples/resource') -var request = require('supertest') +var request = require('../support/supertest') describe('resource', function(){ describe('GET /', function(){ diff --git a/test/acceptance/route-map.js b/test/acceptance/route-map.js index 0bd2a6d32e..0bf2fc45a4 100644 --- a/test/acceptance/route-map.js +++ b/test/acceptance/route-map.js @@ -1,5 +1,5 @@ -var request = require('supertest') +var request = require('../support/supertest') , app = require('../../examples/route-map'); describe('route-map', function(){ diff --git a/test/acceptance/route-separation.js b/test/acceptance/route-separation.js index 867fd29527..2cd0d8abfa 100644 --- a/test/acceptance/route-separation.js +++ b/test/acceptance/route-separation.js @@ -1,6 +1,6 @@ var app = require('../../examples/route-separation') -var request = require('supertest') +var request = require('../support/supertest') describe('route-separation', function () { describe('GET /', function () { diff --git a/test/acceptance/vhost.js b/test/acceptance/vhost.js index 1b633d4b2b..7be1038251 100644 --- a/test/acceptance/vhost.js +++ b/test/acceptance/vhost.js @@ -1,46 +1,50 @@ var app = require('../../examples/vhost') -var request = require('supertest') +var request = require('../support/supertest') -describe('vhost', function(){ - describe('example.com', function(){ - describe('GET /', function(){ - it('should say hello', function(done){ - request(app) - .get('/') - .set('Host', 'example.com') - .expect(200, /hello/i, done) +// vhost with http2 has an issue. +// See also https://github.com/expressjs/vhost/pull/29 +if (!process.env.HTTP2_TEST) { + describe('vhost', function(){ + describe('example.com', function(){ + describe('GET /', function(){ + it('should say hello', function(done){ + request(app) + .get('/') + .set('Host', 'example.com') + .expect(200, /hello/i, done) + }) }) - }) - describe('GET /foo', function(){ - it('should say foo', function(done){ - request(app) - .get('/foo') - .set('Host', 'example.com') - .expect(200, 'requested foo', done) + describe('GET /foo', function(){ + it('should say foo', function(done){ + request(app) + .get('/foo') + .set('Host', 'example.com') + .expect(200, 'requested foo', done) + }) }) }) - }) - describe('foo.example.com', function(){ - describe('GET /', function(){ - it('should redirect to /foo', function(done){ - request(app) - .get('/') - .set('Host', 'foo.example.com') - .expect(302, /Redirecting to http:\/\/example.com:3000\/foo/, done) + describe('foo.example.com', function(){ + describe('GET /', function(){ + it('should redirect to /foo', function(done){ + request(app) + .get('/') + .set('Host', 'foo.example.com') + .expect(302, /Redirecting to http:\/\/example.com:3000\/foo/, done) + }) }) }) - }) - describe('bar.example.com', function(){ - describe('GET /', function(){ - it('should redirect to /bar', function(done){ - request(app) - .get('/') - .set('Host', 'bar.example.com') - .expect(302, /Redirecting to http:\/\/example.com:3000\/bar/, done) + describe('bar.example.com', function(){ + describe('GET /', function(){ + it('should redirect to /bar', function(done){ + request(app) + .get('/') + .set('Host', 'bar.example.com') + .expect(302, /Redirecting to http:\/\/example.com:3000\/bar/, done) + }) }) }) }) -}) +} diff --git a/test/acceptance/web-service.js b/test/acceptance/web-service.js index 521fa9fa28..08fe8e08cc 100644 --- a/test/acceptance/web-service.js +++ b/test/acceptance/web-service.js @@ -1,5 +1,5 @@ -var request = require('supertest') +var request = require('../support/supertest') , app = require('../../examples/web-service'); describe('web-service', function(){ diff --git a/test/app.all.js b/test/app.all.js index e9ef08831d..398ca9e665 100644 --- a/test/app.all.js +++ b/test/app.all.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('app.all()', function(){ it('should add a router per method', function(done){ diff --git a/test/app.head.js b/test/app.head.js index ed8499ce29..0729c67b21 100644 --- a/test/app.head.js +++ b/test/app.head.js @@ -1,6 +1,6 @@ var express = require('../'); -var request = require('supertest'); +var request = require('./support/supertest'); var assert = require('assert'); describe('HEAD', function(){ diff --git a/test/app.js b/test/app.js index 4a18b9bcd6..621a6a270e 100644 --- a/test/app.js +++ b/test/app.js @@ -1,7 +1,7 @@ var assert = require('assert') var express = require('..') -var request = require('supertest') +var request = require('./support/supertest') describe('app', function(){ it('should inherit from event emitter', function(done){ diff --git a/test/app.options.js b/test/app.options.js index b924475038..3cd9d1d3c4 100644 --- a/test/app.options.js +++ b/test/app.options.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('OPTIONS', function(){ it('should default to the routes defined', function(done){ diff --git a/test/app.param.js b/test/app.param.js index 228b4890b5..5ce8c92444 100644 --- a/test/app.param.js +++ b/test/app.param.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('app', function(){ describe('.param(names, fn)', function(){ diff --git a/test/app.request.js b/test/app.request.js index 728043a5a3..48c7c4deec 100644 --- a/test/app.request.js +++ b/test/app.request.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('app', function(){ describe('.request', function(){ @@ -10,6 +10,9 @@ describe('app', function(){ app.request.querystring = function(){ return require('url').parse(this.url).query; }; + if(app.isHttp2Supported){ + app.http2Request.querystring = app.request.querystring; + } app.use(function(req, res){ res.end(req.querystring()); diff --git a/test/app.response.js b/test/app.response.js index c6ea77c820..d6a3c565f5 100644 --- a/test/app.response.js +++ b/test/app.response.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('app', function(){ describe('.response', function(){ @@ -10,6 +10,9 @@ describe('app', function(){ app.response.shout = function(str){ this.send(str.toUpperCase()); }; + if(app.isHttp2Supported){ + app.http2Response.shout = app.response.shout; + } app.use(function(req, res){ res.shout('hey'); @@ -27,10 +30,16 @@ describe('app', function(){ app.response.shout = function(str){ this.send(str.toUpperCase()); }; + if(app.isHttp2Supported){ + app.http2Response.shout = app.response.shout; + } app2.response.shout = function(str){ this.send(str); }; + if(app2.isHttp2Supported){ + app2.http2Response.shout = app2.response.shout; + } app.use(function(req, res){ res.shout('hey'); diff --git a/test/app.route.js b/test/app.route.js index 75e5e0b842..e0659e2a06 100644 --- a/test/app.route.js +++ b/test/app.route.js @@ -1,5 +1,5 @@ var express = require('../'); -var request = require('supertest'); +var request = require('./support/supertest'); describe('app.route', function(){ it('should return a new route', function(done){ diff --git a/test/app.router.js b/test/app.router.js index 0d8da5923d..ecb95b07c3 100644 --- a/test/app.router.js +++ b/test/app.router.js @@ -1,7 +1,7 @@ var after = require('after'); var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') , assert = require('assert') , methods = require('methods'); @@ -50,7 +50,7 @@ describe('app.router', function(){ request(app) [method]('/foo') - .expect('head' == method ? '' : method, done); + .expect('head' == method ? undefined : method, done); }) it('should reject numbers for app.' + method, function(){ diff --git a/test/app.routes.error.js b/test/app.routes.error.js index 9e2a3147cf..f4c52a7021 100644 --- a/test/app.routes.error.js +++ b/test/app.routes.error.js @@ -1,5 +1,5 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('app', function(){ describe('.VERB()', function(){ diff --git a/test/app.use.js b/test/app.use.js index f5d5cc4ff8..af6a65f9c9 100644 --- a/test/app.use.js +++ b/test/app.use.js @@ -2,7 +2,7 @@ var after = require('after'); var assert = require('assert') var express = require('..'); -var request = require('supertest'); +var request = require('./support/supertest'); describe('app', function(){ it('should emit "mount" when mounted', function(done){ diff --git a/test/exports.js b/test/exports.js index 400a958aaa..51b18dcb3a 100644 --- a/test/exports.js +++ b/test/exports.js @@ -1,6 +1,6 @@ var express = require('../'); -var request = require('supertest'); +var request = require('./support/supertest'); describe('exports', function(){ it('should expose Router', function(){ @@ -26,6 +26,9 @@ describe('exports', function(){ it('should permit modifying the .request prototype', function(done){ express.request.foo = function(){ return 'bar'; }; + if(express.isHttp2Supported){ + express.http2Request.foo = express.request.foo; + } var app = express(); app.use(function(req, res, next){ @@ -39,6 +42,9 @@ describe('exports', function(){ it('should permit modifying the .response prototype', function(done){ express.response.foo = function(){ this.send('bar'); }; + if(express.isHttp2Supported){ + express.http2Response.foo = express.response.foo; + } var app = express(); app.use(function(req, res, next){ diff --git a/test/middleware.basic.js b/test/middleware.basic.js index ce59589230..b56f50d4b8 100644 --- a/test/middleware.basic.js +++ b/test/middleware.basic.js @@ -1,6 +1,6 @@ var express = require('../'); -var request = require('supertest'); +var request = require('./support/supertest'); describe('middleware', function(){ describe('.next()', function(){ @@ -29,7 +29,7 @@ describe('middleware', function(){ }); request(app) - .get('/') + .put('/') .set('Content-Type', 'application/json') .send('{"foo":"bar"}') .expect('Content-Type', 'application/json') diff --git a/test/regression.js b/test/regression.js index 5d4509ed6f..913a8b5302 100644 --- a/test/regression.js +++ b/test/regression.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('throw after .end()', function(){ it('should fail gracefully', function(done){ diff --git a/test/req.accepts.js b/test/req.accepts.js index 0df4780e22..d6c17c0bb0 100644 --- a/test/req.accepts.js +++ b/test/req.accepts.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.accepts(type)', function(){ diff --git a/test/req.acceptsCharsets.js b/test/req.acceptsCharsets.js index 2f4574c524..d86d89c9df 100644 --- a/test/req.acceptsCharsets.js +++ b/test/req.acceptsCharsets.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.acceptsCharsets(type)', function(){ diff --git a/test/req.acceptsEncodings.js b/test/req.acceptsEncodings.js index aba8ea5fbe..7a23244db2 100644 --- a/test/req.acceptsEncodings.js +++ b/test/req.acceptsEncodings.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.acceptsEncodingss', function(){ diff --git a/test/req.acceptsLanguages.js b/test/req.acceptsLanguages.js index 1d92f44b2b..7bc5e6a909 100644 --- a/test/req.acceptsLanguages.js +++ b/test/req.acceptsLanguages.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.acceptsLanguages', function(){ diff --git a/test/req.baseUrl.js b/test/req.baseUrl.js index 9ac9d88029..c3274a0b19 100644 --- a/test/req.baseUrl.js +++ b/test/req.baseUrl.js @@ -1,6 +1,6 @@ var express = require('..') -var request = require('supertest') +var request = require('./support/supertest') describe('req', function(){ describe('.baseUrl', function(){ diff --git a/test/req.fresh.js b/test/req.fresh.js index 1aa8fa5b21..eba530c451 100644 --- a/test/req.fresh.js +++ b/test/req.fresh.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.fresh', function(){ diff --git a/test/req.get.js b/test/req.get.js index 109a2d90ce..766499c3e9 100644 --- a/test/req.get.js +++ b/test/req.get.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') , assert = require('assert'); describe('req', function(){ diff --git a/test/req.host.js b/test/req.host.js index 55d6426d2c..b0aa91ac43 100644 --- a/test/req.host.js +++ b/test/req.host.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') describe('req', function(){ describe('.host', function(){ @@ -35,6 +35,7 @@ describe('req', function(){ app.use(function(req, res){ req.headers.host = null; + req.headers[':authority'] = null; res.end(String(req.host)); }); diff --git a/test/req.hostname.js b/test/req.hostname.js index 816cd59799..95f73c41e8 100644 --- a/test/req.hostname.js +++ b/test/req.hostname.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') describe('req', function(){ describe('.hostname', function(){ @@ -35,6 +35,7 @@ describe('req', function(){ app.use(function(req, res){ req.headers.host = null; + req.headers[':authority'] = null; res.end(String(req.hostname)); }); diff --git a/test/req.ip.js b/test/req.ip.js index 1cd255216b..66b21ac29c 100644 --- a/test/req.ip.js +++ b/test/req.ip.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.ip', function(){ diff --git a/test/req.ips.js b/test/req.ips.js index a7d464b846..829936863a 100644 --- a/test/req.ips.js +++ b/test/req.ips.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.ips', function(){ diff --git a/test/req.is.js b/test/req.is.js index a2fce17867..b57fc4ec4a 100644 --- a/test/req.is.js +++ b/test/req.is.js @@ -1,6 +1,6 @@ var express = require('..') -var request = require('supertest') +var request = require('./support/supertest') describe('req.is()', function () { describe('when given a mime type', function () { diff --git a/test/req.path.js b/test/req.path.js index 6ad4009c7d..d579d2b412 100644 --- a/test/req.path.js +++ b/test/req.path.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.path', function(){ diff --git a/test/req.protocol.js b/test/req.protocol.js index 453ad11ca4..ab67854613 100644 --- a/test/req.protocol.js +++ b/test/req.protocol.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.protocol', function(){ diff --git a/test/req.query.js b/test/req.query.js index 1caeaa6ac6..2fc163d778 100644 --- a/test/req.query.js +++ b/test/req.query.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.query', function(){ diff --git a/test/req.range.js b/test/req.range.js index 5443c0658d..488152631c 100644 --- a/test/req.range.js +++ b/test/req.range.js @@ -1,6 +1,6 @@ var express = require('..'); -var request = require('supertest') +var request = require('./support/supertest') describe('req', function(){ describe('.range(size)', function(){ diff --git a/test/req.route.js b/test/req.route.js index 2947b7c3d0..f85486ac5f 100644 --- a/test/req.route.js +++ b/test/req.route.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.route', function(){ diff --git a/test/req.secure.js b/test/req.secure.js index 2025c8786b..f6fa0db849 100644 --- a/test/req.secure.js +++ b/test/req.secure.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.secure', function(){ diff --git a/test/req.signedCookies.js b/test/req.signedCookies.js index 73880b01b4..7a93a4c65a 100644 --- a/test/req.signedCookies.js +++ b/test/req.signedCookies.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') , cookieParser = require('cookie-parser') describe('req', function(){ diff --git a/test/req.stale.js b/test/req.stale.js index 30c9d05d51..43860a7f6e 100644 --- a/test/req.stale.js +++ b/test/req.stale.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.stale', function(){ diff --git a/test/req.subdomains.js b/test/req.subdomains.js index 18e4d80ad3..788f2f8393 100644 --- a/test/req.subdomains.js +++ b/test/req.subdomains.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.subdomains', function(){ @@ -66,6 +66,7 @@ describe('req', function(){ app.use(function(req, res){ req.headers.host = null; + req.headers[':authority'] = null; res.send(req.subdomains); }); diff --git a/test/req.xhr.js b/test/req.xhr.js index 1bbc247104..71d3b84f55 100644 --- a/test/req.xhr.js +++ b/test/req.xhr.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('req', function(){ describe('.xhr', function(){ diff --git a/test/res.append.js b/test/res.append.js index f7f1d55b3c..9d11090b65 100644 --- a/test/res.append.js +++ b/test/res.append.js @@ -1,6 +1,6 @@ var express = require('..') -var request = require('supertest') +var request = require('./support/supertest') var should = require('should') describe('res', function () { diff --git a/test/res.attachment.js b/test/res.attachment.js index 4c3d4aa2f1..65fb5aebea 100644 --- a/test/res.attachment.js +++ b/test/res.attachment.js @@ -1,7 +1,7 @@ var Buffer = require('safe-buffer').Buffer var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('res', function(){ describe('.attachment()', function(){ diff --git a/test/res.clearCookie.js b/test/res.clearCookie.js index 4822057e92..7bf4ad7f44 100644 --- a/test/res.clearCookie.js +++ b/test/res.clearCookie.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('res', function(){ describe('.clearCookie(name)', function(){ diff --git a/test/res.cookie.js b/test/res.cookie.js index 4eeaaf094a..3851eb5be6 100644 --- a/test/res.cookie.js +++ b/test/res.cookie.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') , cookie = require('cookie') , cookieParser = require('cookie-parser') var merge = require('utils-merge'); diff --git a/test/res.download.js b/test/res.download.js index 30215bf676..c81c6366cb 100644 --- a/test/res.download.js +++ b/test/res.download.js @@ -2,7 +2,7 @@ var after = require('after'); var assert = require('assert'); var express = require('..'); -var request = require('supertest'); +var request = require('./support/supertest'); describe('res', function(){ describe('.download(path)', function(){ diff --git a/test/res.format.js b/test/res.format.js index 1e3571bb02..da3c519c68 100644 --- a/test/res.format.js +++ b/test/res.format.js @@ -1,7 +1,7 @@ var after = require('after') var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') , assert = require('assert'); var app1 = express(); diff --git a/test/res.get.js b/test/res.get.js index a53bdc3380..7461b26ebd 100644 --- a/test/res.get.js +++ b/test/res.get.js @@ -1,6 +1,6 @@ var express = require('..'); -var request = require('supertest'); +var request = require('./support/supertest'); describe('res', function(){ describe('.get(field)', function(){ diff --git a/test/res.json.js b/test/res.json.js index c8fd9515f6..a1526c5685 100644 --- a/test/res.json.js +++ b/test/res.json.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') , assert = require('assert'); describe('res', function(){ diff --git a/test/res.jsonp.js b/test/res.jsonp.js index 94aecb1fb7..d4b783384a 100644 --- a/test/res.jsonp.js +++ b/test/res.jsonp.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') , assert = require('assert'); var utils = require('./support/utils'); diff --git a/test/res.links.js b/test/res.links.js index 36630c9ccc..6d677b5524 100644 --- a/test/res.links.js +++ b/test/res.links.js @@ -1,6 +1,6 @@ var express = require('..'); -var request = require('supertest'); +var request = require('./support/supertest'); describe('res', function(){ describe('.links(obj)', function(){ diff --git a/test/res.locals.js b/test/res.locals.js index 3c83e66c54..8c2ee9e12d 100644 --- a/test/res.locals.js +++ b/test/res.locals.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('res', function(){ describe('.locals', function(){ diff --git a/test/res.location.js b/test/res.location.js index c0bfbe8c8e..04c5a7693c 100644 --- a/test/res.location.js +++ b/test/res.location.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('res', function(){ describe('.location(url)', function(){ diff --git a/test/res.redirect.js b/test/res.redirect.js index d7a068af5b..12c3620e36 100644 --- a/test/res.redirect.js +++ b/test/res.redirect.js @@ -1,6 +1,6 @@ var express = require('..'); -var request = require('supertest'); +var request = require('./support/supertest'); var utils = require('./support/utils'); describe('res', function(){ @@ -71,7 +71,7 @@ describe('res', function(){ request(app) .head('/') .expect('Location', 'http://google.com') - .expect(302, '', done) + .expect(302, undefined, done) }) }) diff --git a/test/res.render.js b/test/res.render.js index 643a57002a..0f27d45a40 100644 --- a/test/res.render.js +++ b/test/res.render.js @@ -1,7 +1,7 @@ var express = require('..'); var path = require('path') -var request = require('supertest'); +var request = require('./support/supertest'); var tmpl = require('./support/tmpl'); describe('res', function(){ diff --git a/test/res.send.js b/test/res.send.js index 877e5da9cf..5bf9a6138f 100644 --- a/test/res.send.js +++ b/test/res.send.js @@ -2,7 +2,7 @@ var Buffer = require('safe-buffer').Buffer var express = require('..'); var methods = require('methods'); -var request = require('supertest'); +var request = require('./support/supertest'); var utils = require('./support/utils'); describe('res', function(){ @@ -211,7 +211,7 @@ describe('res', function(){ request(app) .head('/') - .expect('', done); + .expect(undefined, done); }) }) diff --git a/test/res.sendFile.js b/test/res.sendFile.js index 96a2f15a43..2d36bb41f9 100644 --- a/test/res.sendFile.js +++ b/test/res.sendFile.js @@ -1,7 +1,7 @@ var after = require('after'); var express = require('../') - , request = require('supertest') + , request = require('./support/supertest') var onFinished = require('on-finished'); var path = require('path'); var should = require('should'); @@ -101,7 +101,7 @@ describe('res', function(){ app.use(function (req, res) { setImmediate(function () { res.sendFile(path.resolve(fixtures, 'name.txt')); - server.close(cb) + test.app.close(cb); }); test.abort(); }); @@ -111,8 +111,7 @@ describe('res', function(){ cb(); }); - var server = app.listen() - var test = request(server).get('/') + var test = request(app).get('/'); test.expect(200, cb); }) @@ -264,14 +263,13 @@ describe('res', function(){ res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) { should(err).be.ok() err.code.should.equal('ECONNABORTED'); - server.close(cb) + test.app.close(cb); }); }); test.abort(); }); - var server = app.listen() - var test = request(server).get('/') + var test = request(app).get('/') test.expect(200, cb); }) @@ -284,29 +282,31 @@ describe('res', function(){ res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) { should(err).be.ok() err.code.should.equal('ECONNABORTED'); - server.close(cb) + test.app.close(cb); }); }); test.abort(); }); - var server = app.listen() - var test = request(server).get('/') + var test = request(app).get('/') test.expect(200, cb); }) - it('should invoke the callback without error when HEAD', function (done) { - var app = express(); - var cb = after(2, done); + // HEAD with http2 does not support response body. + if (!process.env.HTTP2_TEST) { + it('should invoke the callback without error when HEAD', function (done) { + var app = express(); + var cb = after(2, done); - app.use(function (req, res) { - res.sendFile(path.resolve(fixtures, 'name.txt'), cb); - }); + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'name.txt'), cb); + }); - request(app) - .head('/') - .expect(200, cb); - }); + request(app) + .head('/') + .expect(200, cb); + }); + } it('should invoke the callback without error when 304', function (done) { var app = express(); diff --git a/test/res.sendStatus.js b/test/res.sendStatus.js index c355bc408f..84626fa023 100644 --- a/test/res.sendStatus.js +++ b/test/res.sendStatus.js @@ -1,6 +1,6 @@ var express = require('..') -var request = require('supertest') +var request = require('./support/supertest') describe('res', function () { describe('.sendStatus(statusCode)', function () { diff --git a/test/res.set.js b/test/res.set.js index e46d123947..d18e8c579f 100644 --- a/test/res.set.js +++ b/test/res.set.js @@ -1,6 +1,6 @@ var express = require('..'); -var request = require('supertest'); +var request = require('./support/supertest'); describe('res', function(){ describe('.set(field, value)', function(){ diff --git a/test/res.status.js b/test/res.status.js index 8c173a645c..fc9da0a808 100644 --- a/test/res.status.js +++ b/test/res.status.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('res', function(){ describe('.status(code)', function(){ diff --git a/test/res.type.js b/test/res.type.js index cc1dd08d41..fcbd71b349 100644 --- a/test/res.type.js +++ b/test/res.type.js @@ -1,6 +1,6 @@ var express = require('../') - , request = require('supertest'); + , request = require('./support/supertest'); describe('res', function(){ describe('.type(str)', function(){ diff --git a/test/res.vary.js b/test/res.vary.js index a55d2151df..037106bff2 100644 --- a/test/res.vary.js +++ b/test/res.vary.js @@ -1,6 +1,6 @@ var express = require('..'); -var request = require('supertest'); +var request = require('./support/supertest'); var utils = require('./support/utils'); describe('res.vary()', function(){ diff --git a/test/support/http2wrapper.js b/test/support/http2wrapper.js new file mode 100644 index 0000000000..84da937be7 --- /dev/null +++ b/test/support/http2wrapper.js @@ -0,0 +1,186 @@ +'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) { + this.session.socket.setNoDelay(bool); +} + +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.js b/test/support/supertest.js new file mode 100644 index 0000000000..52039e8766 --- /dev/null +++ b/test/support/supertest.js @@ -0,0 +1,33 @@ +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.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 From 07194ece88238ce04a4afa009569e5ddd3b1be4d Mon Sep 17 00:00:00 2001 From: sogaani Date: Wed, 29 Aug 2018 21:23:11 +0900 Subject: [PATCH 3/6] http2 test with appveyor --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 193660af71..6cd9724f62 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,6 +10,8 @@ environment: - nodejs_version: "6.11" - nodejs_version: "7.10" - nodejs_version: "8.4" + - nodejs_version: "8" + HTTP2_TEST: 1 cache: - node_modules install: From 2a78f4cb36ff63a18f1264ce1a0848c9bfedbf2b Mon Sep 17 00:00:00 2001 From: sogaani Date: Mon, 10 Sep 2018 20:51:28 +0900 Subject: [PATCH 4/6] Fix res.sendFile with http2 --- lib/responseDecorator.js | 3 ++- test/res.sendFile.js | 23 ++++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/responseDecorator.js b/lib/responseDecorator.js index 2469c14ee7..d31f1c0f18 100644 --- a/lib/responseDecorator.js +++ b/lib/responseDecorator.js @@ -933,7 +933,6 @@ function setMethods(res) { file.on('error', onerror); file.on('file', onfile); file.on('stream', onstream); - onFinished(res, onfinish); if (options.headers) { // set headers on successful transfer @@ -950,6 +949,8 @@ function setMethods(res) { // pipe file.pipe(res); + + onFinished(res, onfinish); } return res; diff --git a/test/res.sendFile.js b/test/res.sendFile.js index 2d36bb41f9..656263ee9b 100644 --- a/test/res.sendFile.js +++ b/test/res.sendFile.js @@ -292,21 +292,18 @@ describe('res', function(){ test.expect(200, cb); }) - // HEAD with http2 does not support response body. - if (!process.env.HTTP2_TEST) { - it('should invoke the callback without error when HEAD', function (done) { - var app = express(); - var cb = after(2, done); - - app.use(function (req, res) { - res.sendFile(path.resolve(fixtures, 'name.txt'), cb); - }); + it('should invoke the callback without error when HEAD', function (done) { + var app = express(); + var cb = after(2, done); - request(app) - .head('/') - .expect(200, cb); + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'name.txt'), cb); }); - } + + request(app) + .head('/') + .expect(200, cb); + }); it('should invoke the callback without error when 304', function (done) { var app = express(); From b2bb3a46efafb2a2b53221c6a532d6da7d94a844 Mon Sep 17 00:00:00 2001 From: sogaani Date: Fri, 14 Sep 2018 08:21:20 +0900 Subject: [PATCH 5/6] Fix http2 test with node 10 --- .travis.yml | 4 ++++ test/support/http2wrapper.js | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ebc301bba6..1d49d3e55f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,10 @@ matrix: env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" - node_js: "9" env: HTTP2_TEST=1 + - node_js: "10" + env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" + - node_js: "10" + env: HTTP2_TEST=1 allow_failures: # Allow the nightly installs to fail - env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" diff --git a/test/support/http2wrapper.js b/test/support/http2wrapper.js index 84da937be7..4e87068fc1 100644 --- a/test/support/http2wrapper.js +++ b/test/support/http2wrapper.js @@ -80,7 +80,9 @@ Request.prototype.createUnixConnection = function (authority, options) { } Request.prototype.setNoDelay = function (bool) { - this.session.socket.setNoDelay(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 () { From 826ca6d8d8e0f0138849271af58978725bd66e6b Mon Sep 17 00:00:00 2001 From: sogaani Date: Fri, 14 Sep 2018 08:33:12 +0900 Subject: [PATCH 6/6] http2 test only with node 10 --- .travis.yml | 4 ---- appveyor.yml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1d49d3e55f..c791c15403 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,12 +14,8 @@ matrix: include: - node_js: "8" env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" - - node_js: "8" - env: HTTP2_TEST=1 - node_js: "9" env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" - - node_js: "9" - env: HTTP2_TEST=1 - node_js: "10" env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" - node_js: "10" diff --git a/appveyor.yml b/appveyor.yml index 6cd9724f62..6990ecb15d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,7 +10,7 @@ environment: - nodejs_version: "6.11" - nodejs_version: "7.10" - nodejs_version: "8.4" - - nodejs_version: "8" + - nodejs_version: "10" HTTP2_TEST: 1 cache: - node_modules