From 58b88f17c91c0747a5691bef5d906dcae8963375 Mon Sep 17 00:00:00 2001 From: pini Date: Wed, 9 Aug 2017 16:59:02 +0300 Subject: [PATCH 01/12] Initial support proposal for http2 --- lib/application.js | 8 + lib/express.js | 21 +- lib/middleware/init.js | 17 +- lib/request.js | 853 ++++++++++--------- lib/response.js | 1817 ++++++++++++++++++++-------------------- lib/utils.js | 14 + test/exports.js | 8 +- 7 files changed, 1408 insertions(+), 1330 deletions(-) diff --git a/lib/application.js b/lib/application.js index 1abe8d08f5..628412bd1e 100644 --- a/lib/application.js +++ b/lib/application.js @@ -22,6 +22,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 deprecate = require('depd')('express'); @@ -99,6 +100,13 @@ 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 diff --git a/lib/express.js b/lib/express.js index 187e4e2d7c..67b4f6f8d5 100644 --- a/lib/express.js +++ b/lib/express.js @@ -19,7 +19,7 @@ var Route = require('./router/route'); 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); }; @@ -42,15 +42,28 @@ function createApplication() { mixin(app, proto, false); // expose the prototype that will get set on requests - app.request = Object.create(req, { + app.request = Object.create(req.req, { app: { configurable: true, enumerable: true, writable: true, value: app } }) // expose the prototype that will get set on responses - app.response = Object.create(res, { + app.response = Object.create(res.res, { app: { configurable: true, enumerable: true, writable: true, value: app } }) + if (isHttp2Supported) { + app.http2Request = Object.create(req.http2Req, { + app: { configurable: true, enumerable: true, writable: true, value: app } + }); + + app.http2Response = Object.create(res.http2Res, { + app: { configurable: true, enumerable: true, writable: true, value: app } + }); + } + + + + app.init(); return app; } diff --git a/lib/middleware/init.js b/lib/middleware/init.js index dfd042747b..68bcdf6f1a 100644 --- a/lib/middleware/init.js +++ b/lib/middleware/init.js @@ -7,14 +7,18 @@ */ 'use strict'; - /** * Module dependencies. * @private */ -var setPrototypeOf = require('setprototypeof') +var setPrototypeOf = require('setprototypeof'); +var isHttp2Supported = require('../utils').isHttp2Supported; +var http2Request = null; +if (isHttp2Supported) { + http2Request = require('http2').Http2ServerRequest; +} /** * Initialization middleware, exposing the * request and response to each other, as well @@ -31,9 +35,14 @@ exports.init = function(app){ req.res = res; res.req = req; req.next = next; + if (isHttp2Supported && req instanceof http2Request) { + setPrototypeOf(req, app.http2Request) + setPrototypeOf(res, app.http2Response) + } else { + setPrototypeOf(req, app.request) + setPrototypeOf(res, app.response) + } - setPrototypeOf(req, app.request) - setPrototypeOf(res, app.response) res.locals = res.locals || Object.create(null); diff --git a/lib/request.js b/lib/request.js index 3432e6776f..774ac680c9 100644 --- a/lib/request.js +++ b/lib/request.js @@ -22,22 +22,31 @@ var fresh = require('fresh'); var parseRange = require('range-parser'); var parse = require('parseurl'); var proxyaddr = require('proxy-addr'); - +var isHttp2Supported = require('./utils').isHttp2Supported; /** * Request prototype. * @public */ var req = Object.create(http.IncomingMessage.prototype) - /** * Module exports. * @public */ -module.exports = req +module.exports = { + req: setMethods(req) +}; + +if (isHttp2Supported) { + var http2 = require('http2'); + var http2Req = Object.create(http2.Http2ServerRequest.prototype); + module.exports['http2Req'] = setMethods(http2Req); +} -/** +function setMethods(req) { + + /** * Return request header. * * The `Referrer` header field is special-cased, @@ -61,444 +70,450 @@ module.exports = req * @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); -}; - -req.acceptsCharset = deprecate.function(req.acceptsCharsets, - 'req.acceptsCharset: Use acceptsCharsets instead'); - -/** - * 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); -}; - -req.acceptsLanguage = deprecate.function(req.acceptsLanguages, - 'req.acceptsLanguage: Use acceptsLanguages instead'); - -/** - * 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); -}; - -/** - * Return the value of param `name` when present or `defaultValue`. - * - * - Checks route placeholders, ex: _/user/:id_ - * - Checks body params, ex: id=12, {"id":12} - * - Checks query string params, ex: ?id=12 - * - * To utilize request bodies, `req.body` - * should be an object. This can be done by using - * the `bodyParser()` middleware. - * - * @param {String} name - * @param {Mixed} [defaultValue] - * @return {String} - * @public - */ - -req.param = function param(name, defaultValue) { - var params = this.params || {}; - var body = this.body || {}; - var query = this.query || {}; - - var args = arguments.length === 1 - ? 'name' - : 'name, default'; - deprecate('req.param(' + args + '): Use req.params, req.body, or req.query instead'); - - if (null != params[name] && params.hasOwnProperty(name)) return params[name]; - if (null != body[name]) return body[name]; - if (null != query[name]) return query[name]; - - return defaultValue; -}; - -/** - * 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]; + 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); + }; + + req.acceptsCharset = deprecate.function(req.acceptsCharsets, + 'req.acceptsCharset: Use acceptsCharsets instead'); + + /** + * 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); + }; + + req.acceptsLanguage = deprecate.function(req.acceptsLanguages, + 'req.acceptsLanguage: Use acceptsLanguages instead'); + + /** + * 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); + }; + + /** + * Return the value of param `name` when present or `defaultValue`. + * + * - Checks route placeholders, ex: _/user/:id_ + * - Checks body params, ex: id=12, {"id":12} + * - Checks query string params, ex: ?id=12 + * + * To utilize request bodies, `req.body` + * should be an object. This can be done by using + * the `bodyParser()` middleware. + * + * @param {String} name + * @param {Mixed} [defaultValue] + * @return {String} + * @public + */ + + req.param = function param(name, defaultValue) { + var params = this.params || {}; + var body = this.body || {}; + var query = this.query || {}; + + var args = arguments.length === 1 + ? 'name' + : 'name, default'; + deprecate('req.param(' + args + '): Use req.params, req.body, or req.query instead'); + + if (null != params[name] && params.hasOwnProperty(name)) return params[name]; + if (null != body[name]) return body[name]; + if (null != query[name]) return query[name]; + + return defaultValue; + }; + + /** + * 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. - proto = this.get('X-Forwarded-Proto') || proto; - return proto.split(/\s*,\s*/)[0]; -}); - -/** - * 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 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; + } - return subdomains.slice(offset); -}); + // Note: X-Forwarded-Proto is normally only ever a + // single value, but this is to be safe. + proto = this.get('X-Forwarded-Proto') || proto; + return proto.split(/\s*,\s*/)[0]; + }); -/** - * Short-hand for `url.parse(req.url).pathname`. - * - * @return {String} - * @public - */ + /** + * Short-hand for: + * + * req.protocol === 'https' + * + * @return {Boolean} + * @public + */ + + defineGetter(req, 'secure', function secure() { + return this.protocol === 'https'; + }); -defineGetter(req, 'path', function path() { - return parse(this).pathname; -}); + /** + * 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); + }); -/** - * 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} - * @public - */ + /** + * 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 + }); -defineGetter(req, 'hostname', function hostname(){ - var trust = this.app.get('trust proxy fn'); - var host = this.get('X-Forwarded-Host'); + /** + * 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); + }); - if (!host || !trust(this.connection.remoteAddress, 0)) { - host = this.get('Host'); + /** + * Short-hand for `url.parse(req.url).pathname`. + * Check for IncomingMessage is to avoid http2 stack overflow + * @return {String} + * @public + */ + if (req instanceof http.IncomingMessage) { + defineGetter(req, 'path', function path() { + return parse(this).pathname; + }); } - if (!host) return; - // IPv6 literal support - var offset = host[0] === '[' - ? host.indexOf(']') + 1 - : 0; - var index = host.indexOf(':', offset); + /** + * 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} + * @public + */ - return index !== -1 - ? host.substring(0, index) - : host; -}); + defineGetter(req, 'hostname', function hostname() { + var trust = this.app.get('trust proxy fn'); + var host = this.get('X-Forwarded-Host'); -// TODO: change req.host to return host in next major + if (!host || !trust(this.connection.remoteAddress, 0)) { + host = this.get('Host'); + } -defineGetter(req, 'host', deprecate.function(function host(){ - return this.hostname; -}, 'req.host: Use req.hostname instead')); + if (!host) return; -/** - * Check if the request is fresh, aka - * Last-Modified and/or the ETag - * still match. - * - * @return {Boolean} - * @public - */ + // IPv6 literal support + var offset = host[0] === '[' + ? host.indexOf(']') + 1 + : 0; + var index = host.indexOf(':', offset); -defineGetter(req, 'fresh', function(){ - var method = this.method; - var res = this.res - var status = res.statusCode + return index !== -1 + ? host.substring(0, index) + : host; + }); - // GET or HEAD for weak freshness validation only - if ('GET' !== method && 'HEAD' !== method) return false; + // TODO: change req.host to return host in next major + + defineGetter(req, 'host', deprecate.function(function host() { + return this.hostname; + }, 'req.host: Use req.hostname instead')); + + /** + * 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') + }) + } - // 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; + }); - 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 is stale, aka - * "Last-Modified" and / or the "ETag" for the - * resource has changed. - * - * @return {Boolean} - * @public - */ + /** + * Check if the request was an _XMLHttpRequest_. + * + * @return {Boolean} + * @public + */ -defineGetter(req, 'stale', function stale(){ - return !this.fresh; -}); + defineGetter(req, 'xhr', function xhr() { + var val = this.get('X-Requested-With') || ''; + return val.toLowerCase() === 'xmlhttprequest'; + }); -/** - * Check if the request was an _XMLHttpRequest_. - * - * @return {Boolean} - * @public - */ + return req; +} -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. diff --git a/lib/response.js b/lib/response.js index b852a60e2f..6f3e376a32 100644 --- a/lib/response.js +++ b/lib/response.js @@ -26,1040 +26,1059 @@ var sign = require('cookie-signature').sign; var normalizeType = require('./utils').normalizeType; var normalizeTypes = require('./utils').normalizeTypes; var setCharset = require('./utils').setCharset; +var isHttp2Supported = require('./utils').isHttp2Supported; 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 http2 = isHttp2Supported ? require('http2') : null; /** * Response prototype. * @public */ -var res = Object.create(http.ServerResponse.prototype) - +var res = Object.create(http.ServerResponse.prototype); /** * Module exports. * @public */ -module.exports = res - -/** - * Module variables. - * @private - */ - -var charsetRegExp = /;\s*charset\s*=/; +module.exports = { + res: setMethods(res) +} -/** - * Set status `code`. - * - * @param {Number} code - * @return {ServerResponse} - * @public - */ +if (isHttp2Supported) { + var http2Res = Object.create(http2.Http2ServerResponse.prototype); + module.exports['http2Res'] = setMethods(http2Res); +} -res.status = function status(code) { - this.statusCode = code; - return this; -}; +function setMethods(res) { + /** + * Module variables. + * @private + */ -/** - * 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 - */ + var charsetRegExp = /;\s*charset\s*=/; -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(', ')); -}; + /** + * Set status `code`. + * + * @param {Number} code + * @return {ServerResponse} + * @public + */ -/** - * Send a response. - * - * Examples: - * - * res.send(new Buffer('wahoo')); - * res.send({ some: 'json' }); - * res.send('

some html

'); - * - * @param {string|number|boolean|object|Buffer} body - * @public - */ + res.status = function status(code) { + this.statusCode = code; + return this; + }; -res.send = function send(body) { - var chunk = body; - var encoding; - var len; - var req = this.req; - var type; - - // settings - var app = this.app; - - // allow status / body - if (arguments.length === 2) { - // res.send(body, status) backwards compat - if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') { - deprecate('res.send(body, status): Use res.status(status).send(body) instead'); - this.statusCode = arguments[1]; - } else { - deprecate('res.send(status, body): Use res.status(status).send(body) instead'); - this.statusCode = arguments[0]; - chunk = arguments[1]; - } - } + /** + * 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(', ')); + }; - // disambiguate res.send(status) and res.send(status, num) - if (typeof chunk === 'number' && arguments.length === 1) { - // res.send(status) will set status message as text string - if (!this.get('Content-Type')) { - this.type('txt'); + /** + * Send a response. + * + * Examples: + * + * res.send(new Buffer('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 len; + var req = this.req; + var type; + + // settings + var app = this.app; + + // allow status / body + if (arguments.length === 2) { + // res.send(body, status) backwards compat + if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') { + deprecate('res.send(body, status): Use res.status(status).send(body) instead'); + this.statusCode = arguments[1]; + } else { + deprecate('res.send(status, body): Use res.status(status).send(body) instead'); + this.statusCode = arguments[0]; + chunk = arguments[1]; + } } - deprecate('res.send(status): Use res.sendStatus(status) instead'); - this.statusCode = chunk; - chunk = statuses[chunk] - } - - switch (typeof chunk) { - // string defaulting to html - case 'string': + // disambiguate res.send(status) and res.send(status, num) + if (typeof chunk === 'number' && arguments.length === 1) { + // res.send(status) will set status message as text 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); + this.type('txt'); } - 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')); + deprecate('res.send(status): Use res.sendStatus(status) instead'); + this.statusCode = chunk; + chunk = statuses[chunk] } - } - // populate Content-Length - if (chunk !== undefined) { - if (!Buffer.isBuffer(chunk)) { - // convert chunk to Buffer; saves later double conversions - chunk = new Buffer(chunk, encoding); - encoding = undefined; + 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; } - len = chunk.length; - this.set('Content-Length', len); - } + // write strings in utf-8 + if (typeof chunk === 'string') { + encoding = 'utf8'; + type = this.get('Content-Type'); - // populate ETag - var etag; - var generateETag = len !== undefined && app.get('etag fn'); - if (typeof generateETag === 'function' && !this.get('ETag')) { - if ((etag = generateETag(chunk, encoding))) { - this.set('ETag', etag); + // reflect this in content-type + if (typeof type === 'string') { + this.set('Content-Type', setCharset(type, 'utf-8')); + } } - } - - // 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 = ''; - } + // populate Content-Length + if (chunk !== undefined) { + if (!Buffer.isBuffer(chunk)) { + // convert chunk to Buffer; saves later double conversions + chunk = new Buffer(chunk, encoding); + encoding = undefined; + } - if (req.method === 'HEAD') { - // skip body for HEAD - this.end(); - } else { - // respond - this.end(chunk, encoding); - } + len = chunk.length; + this.set('Content-Length', len); + } - return this; -}; + // populate ETag + var etag; + var generateETag = len !== undefined && app.get('etag fn'); + if (typeof generateETag === 'function' && !this.get('ETag')) { + if ((etag = generateETag(chunk, encoding))) { + this.set('ETag', etag); + } + } -/** - * Send JSON response. - * - * Examples: - * - * res.json(null); - * res.json({ user: 'tj' }); - * - * @param {string|number|boolean|object} obj - * @public - */ + // freshness + if (req.fresh) this.statusCode = 304; -res.json = function json(obj) { - var val = obj; + // strip irrelevant headers + if (204 === this.statusCode || 304 === this.statusCode) { + this.removeHeader('Content-Type'); + this.removeHeader('Content-Length'); + this.removeHeader('Transfer-Encoding'); + chunk = ''; + } - // allow status / body - if (arguments.length === 2) { - // res.json(body, status) backwards compat - if (typeof arguments[1] === 'number') { - deprecate('res.json(obj, status): Use res.status(status).json(obj) instead'); - this.statusCode = arguments[1]; + if (req.method === 'HEAD') { + // skip body for HEAD + this.end(); } else { - deprecate('res.json(status, obj): Use res.status(status).json(obj) instead'); - this.statusCode = arguments[0]; - val = arguments[1]; + // respond + this.end(chunk, encoding); } - } - - // settings - var app = this.app; - var replacer = app.get('json replacer'); - var spaces = app.get('json spaces'); - var body = stringify(val, replacer, spaces); - // content-type - if (!this.get('Content-Type')) { - this.set('Content-Type', 'application/json'); - } - - return this.send(body); -}; + return this; + }; -/** - * Send JSON response with JSONP callback support. - * - * Examples: - * - * res.jsonp(null); - * res.jsonp({ user: 'tj' }); - * - * @param {string|number|boolean|object} obj - * @public - */ + /** + * Send JSON response. + * + * Examples: + * + * res.json(null); + * res.json({ user: 'tj' }); + * + * @param {string|number|boolean|object} obj + * @public + */ + + res.json = function json(obj) { + var val = obj; + + // allow status / body + if (arguments.length === 2) { + // res.json(body, status) backwards compat + if (typeof arguments[1] === 'number') { + deprecate('res.json(obj, status): Use res.status(status).json(obj) instead'); + this.statusCode = arguments[1]; + } else { + deprecate('res.json(status, obj): Use res.status(status).json(obj) instead'); + this.statusCode = arguments[0]; + val = arguments[1]; + } + } -res.jsonp = function jsonp(obj) { - var val = obj; + // settings + var app = this.app; + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = stringify(val, replacer, spaces); - // allow status / body - if (arguments.length === 2) { - // res.json(body, status) backwards compat - if (typeof arguments[1] === 'number') { - deprecate('res.jsonp(obj, status): Use res.status(status).json(obj) instead'); - this.statusCode = arguments[1]; - } else { - deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead'); - this.statusCode = arguments[0]; - val = arguments[1]; + // content-type + if (!this.get('Content-Type')) { + this.set('Content-Type', 'application/json'); } - } - // settings - var app = this.app; - var replacer = app.get('json replacer'); - var spaces = app.get('json spaces'); - var body = stringify(val, replacer, spaces); - 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'); - } + return this.send(body); + }; - // fixup callback - if (Array.isArray(callback)) { - callback = callback[0]; + if (isHttp2Supported && res instanceof http2.Http2ServerResponse) { + Object.defineProperty(res, 'statusMessage', { + set: function (x) { + //Empty setter for statusMessage + } + }); } - // jsonp - if (typeof callback === 'string' && callback.length !== 0) { - this.charset = 'utf-8'; - this.set('X-Content-Type-Options', 'nosniff'); - this.set('Content-Type', 'text/javascript'); + /** + * 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) { + var val = obj; + + // allow status / body + if (arguments.length === 2) { + // res.json(body, status) backwards compat + if (typeof arguments[1] === 'number') { + deprecate('res.jsonp(obj, status): Use res.status(status).json(obj) instead'); + this.statusCode = arguments[1]; + } else { + deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead'); + this.statusCode = arguments[0]; + val = arguments[1]; + } + } - // restrict callback charset - callback = callback.replace(/[^\[\]\w$.]/g, ''); + // settings + var app = this.app; + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = stringify(val, replacer, spaces); + var callback = this.req.query[app.get('jsonp callback name')]; - // replace chars not allowed in JavaScript that are in JSON - body = body - .replace(/\u2028/g, '\\u2028') - .replace(/\u2029/g, '\\u2029'); + // content-type + if (!this.get('Content-Type')) { + this.set('X-Content-Type-Options', 'nosniff'); + this.set('Content-Type', 'application/json'); + } - // 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 + ');'; - } + // fixup callback + if (Array.isArray(callback)) { + callback = callback[0]; + } - return this.send(body); -}; + // jsonp + if (typeof callback === 'string' && callback.length !== 0) { + this.charset = 'utf-8'; + this.set('X-Content-Type-Options', 'nosniff'); + this.set('Content-Type', 'text/javascript'); -/** - * 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 - */ + // restrict callback charset + callback = callback.replace(/[^\[\]\w$.]/g, ''); -res.sendStatus = function sendStatus(statusCode) { - var body = statuses[statusCode] || String(statusCode) + // replace chars not allowed in JavaScript that are in JSON + body = body + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); - this.statusCode = statusCode; - this.type('txt'); + // 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); -}; + 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 - */ + /** + * 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); + }; -res.sendFile = function sendFile(path, options, callback) { - var done = callback; - var req = this.req; - var res = this; - var next = req.next; - var opts = options || {}; + /** + * 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'); + } - if (!path) { - throw new TypeError('path argument is required to res.sendFile'); - } + // support function as second arg + if (typeof options === 'function') { + done = options; + opts = {}; + } - // support function as second arg - if (typeof options === 'function') { - done = options; - opts = {}; - } + if (!opts.root && !isAbsolute(path)) { + throw new TypeError('path must be absolute or specify root to res.sendFile'); + } - if (!opts.root && !isAbsolute(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); - // 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(); - // 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); + } + }); + }; - // next() all but write errors - if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') { - next(err); + /** + * 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 (path, options, callback) { + var done = callback; + var req = this.req; + var res = this; + var next = req.next; + var opts = options || {}; + + // support function as second arg + if (typeof options === 'function') { + done = options; + opts = {}; } - }); -}; - -/** - * 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 (path, options, callback) { - var done = callback; - var req = this.req; - var res = this; - var next = req.next; - var opts = options || {}; - - // support function as second arg - if (typeof options === 'function') { - done = options; - opts = {}; - } + // create file stream + var file = send(req, path, opts); - // create file stream - var file = send(req, path, opts); + // transfer + sendfile(res, file, opts, function (err) { + if (done) return done(err); + if (err && err.code === 'EISDIR') return next(); - // 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 !== 'ECONNABORT' && err.syscall !== 'write') { + next(err); + } + }); + }; - // next() all but write errors - if (err && err.code !== 'ECONNABORT' && err.syscall !== 'write') { - next(err); + res.sendfile = deprecate.function(res.sendfile, + 'res.sendfile: Use res.sendFile instead'); + + /** + * 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. + * + * This method uses `res.sendfile()`. + * + * @public + */ + + res.download = function download(path, filename, callback) { + var done = callback; + var name = filename; + + // support function as second arg + if (typeof filename === 'function') { + done = filename; + name = null; } - }); -}; -res.sendfile = deprecate.function(res.sendfile, - 'res.sendfile: Use res.sendFile instead'); + // set Content-Disposition when file is sent + var headers = { + 'Content-Disposition': contentDisposition(name || path) + }; -/** - * 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. - * - * This method uses `res.sendfile()`. - * - * @public - */ - -res.download = function download(path, filename, callback) { - var done = callback; - var name = filename; - - // support function as second arg - if (typeof filename === 'function') { - done = filename; - name = null; - } + // Resolve the full path for sendFile + var fullPath = resolve(path); - // set Content-Disposition when file is sent - var headers = { - 'Content-Disposition': contentDisposition(name || path) + return this.sendFile(fullPath, { headers: headers }, done); }; - // Resolve the full path for sendFile - var fullPath = resolve(path); - - return this.sendFile(fullPath, { headers: headers }, 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; + /** + * 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-Type', ct); -}; + return this; + }; -/** - * 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 - */ + /** + * 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)); + } -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); - } + this.set('Content-Disposition', contentDisposition(filename)); - return this; -}; + return this; + }; -/** - * Set _Content-Disposition_ header to _attachment_ with optional `filename`. - * - * @param {String} filename - * @return {ServerResponse} - * @public - */ + /** + * 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]; + } -res.attachment = function attachment(filename) { - if (filename) { - this.type(extname(filename)); - } + return this.set(field, value); + }; - this.set('Content-Disposition', contentDisposition(filename)); + /** + * 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(); + } + } - return this; -}; + 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); + }; -/** - * 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 - */ + /** + * Clear cookie `name`. + * + * @param {String} name + * @param {Object} [options] + * @return {ServerResponse} for chaining + * @public + */ -res.append = function append(field, val) { - var prev = this.get(field); - var value = val; + res.clearCookie = function clearCookie(name, options) { + var opts = merge({ expires: new Date(1), path: '/' }, options); - 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.cookie(name, '', opts); + }; - return this.set(field, value); -}; + /** + * 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'); + } -/** - * 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 - */ + var val = typeof value === 'object' + ? 'j:' + JSON.stringify(value) + : String(value); -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(); - } + if (signed) { + val = 's:' + sign(val, secret); } - this.setHeader(field, value); - } else { - for (var key in field) { - this.set(key, field[key]); + if ('maxAge' in opts) { + opts.expires = new Date(Date.now() + opts.maxAge); + opts.maxAge /= 1000; } - } - 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 (opts.path == null) { + opts.path = '/'; + } - if ('maxAge' in opts) { - opts.expires = new Date(Date.now() + opts.maxAge); - opts.maxAge /= 1000; - } + this.append('Set-Cookie', cookie.serialize(name, String(val), opts)); - if (opts.path == null) { - opts.path = '/'; - } + return this; + }; - this.append('Set-Cookie', cookie.serialize(name, String(val), opts)); + /** + * 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') || '/'; + } - return this; -}; + // set location + return this.set('Location', encodeUrl(loc)); + }; -/** - * 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 - */ + /** + * 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) { + if (typeof arguments[0] === 'number') { + status = arguments[0]; + address = arguments[1]; + } else { + deprecate('res.redirect(url, status): Use res.redirect(status, url) instead'); + status = arguments[1]; + } + } -res.location = function location(url) { - var loc = url; + // Set location header + address = this.location(address).get('Location'); - // "back" is an alias for the referrer - if (url === 'back') { - loc = this.req.get('Referrer') || '/'; - } + // Support text/{plain,html} by default + this.format({ + text: function () { + body = statuses[status] + '. Redirecting to ' + address + }, - // set location - return this.set('Location', encodeUrl(loc)); -}; + html: function () { + var u = escapeHtml(address); + body = '

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

' + }, -/** - * 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 - */ + default: function () { + body = ''; + } + }); -res.redirect = function redirect(url) { - var address = url; - var body; - var status = 302; + // Respond + this.statusCode = status; + this.set('Content-Length', Buffer.byteLength(body)); - // allow status / url - if (arguments.length === 2) { - if (typeof arguments[0] === 'number') { - status = arguments[0]; - address = arguments[1]; + if (this.req.method === 'HEAD') { + this.end(); } else { - deprecate('res.redirect(url, status): Use res.redirect(status, url) instead'); - status = arguments[1]; + this.end(body); } - } - - // 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 = ''; + /** + * 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) { + // checks for back-compat + if (!field || (Array.isArray(field) && !field.length)) { + deprecate('res.vary(): Provide a field name'); + return this; } - }); - // Respond - this.statusCode = status; - this.set('Content-Length', Buffer.byteLength(body)); + vary(this, field); - 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){ - // checks for back-compat - if (!field || (Array.isArray(field) && !field.length)) { - deprecate('res.vary(): Provide a field name'); return this; - } - - 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 - */ + /** + * 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 = {}; + } -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; - // 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); + }; - // default callback to respond - done = done || function (err, str) { - if (err) return req.next(err); - self.send(str); + // render + app.render(view, opts, done); }; - // render - app.render(view, opts, done); -}; + // pipe the send file stream + function sendfile(res, file, options, callback) { + var done = false; + var streaming; -// 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; - // request aborted - function onaborted() { - if (done) return; - done = true; + var err = new Error('Request aborted'); + err.code = 'ECONNABORTED'; + callback(err); + } - var err = new Error('Request aborted'); - err.code = 'ECONNABORTED'; - callback(err); - } + // directory + function ondirectory() { + if (done) return; + done = true; - // directory - function ondirectory() { - if (done) return; - done = true; + var err = new Error('EISDIR, read'); + err.code = 'EISDIR'; + callback(err); + } - var err = new Error('EISDIR, read'); - err.code = 'EISDIR'; - callback(err); - } + // errors + function onerror(err) { + if (done) return; + done = true; + callback(err); + } - // errors - function onerror(err) { - if (done) return; - done = true; - callback(err); - } + // ended + function onend() { + if (done) return; + done = true; + callback(); + } - // ended - function onend() { - if (done) return; - done = true; - callback(); - } + // file + function onfile() { + streaming = false; + } - // file - function onfile() { - streaming = false; - } + // finished + function onfinish(err) { + if (err && err.code === 'ECONNRESET') return onaborted(); + if (err) return onerror(err); + if (done) return; - // 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; + } - setImmediate(function () { - if (streaming !== false && !done) { - onaborted(); - return; - } + if (done) return; + done = true; + callback(); + }); + } - if (done) return; - done = true; - callback(); - }); - } + // streaming + function onstream() { + streaming = true; + } - // 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]); + } + }); + } - 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); } - // pipe - file.pipe(res); + return res; } /** diff --git a/lib/utils.js b/lib/utils.js index ae2a7f862d..679c0f1298 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -21,7 +21,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`. * @@ -249,6 +259,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. * diff --git a/test/exports.js b/test/exports.js index 2a80eedbbe..5047c78ca2 100644 --- a/test/exports.js +++ b/test/exports.js @@ -13,11 +13,11 @@ describe('exports', function(){ }) it('should expose the request prototype', function(){ - express.request.accepts.should.be.a.Function() + express.request.req.accepts.should.be.a.Function() }) it('should expose the response prototype', function(){ - express.response.send.should.be.a.Function() + express.response.res.send.should.be.a.Function() }) it('should permit modifying the .application prototype', function(){ @@ -26,7 +26,7 @@ describe('exports', function(){ }) it('should permit modifying the .request prototype', function(done){ - express.request.foo = function(){ return 'bar'; }; + express.request.req.foo = function(){ return 'bar'; }; var app = express(); app.use(function(req, res, next){ @@ -39,7 +39,7 @@ describe('exports', function(){ }) it('should permit modifying the .response prototype', function(done){ - express.response.foo = function(){ this.send('bar'); }; + express.response.res.foo = function(){ this.send('bar'); }; var app = express(); app.use(function(req, res, next){ From 43f7c0127f3d8f1cfda0cdef72f54545774549c9 Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Fri, 1 Sep 2017 12:11:11 +0300 Subject: [PATCH 02/12] Seperate to files and revert breaking api --- lib/express.js | 10 +- lib/http2Request.js | 45 ++ lib/http2Response.js | 47 ++ lib/request.js | 511 +----------------- lib/requestDecorator.js | 506 ++++++++++++++++++ lib/response.js | 1074 +------------------------------------- lib/responseDecorator.js | 1073 +++++++++++++++++++++++++++++++++++++ lib/utils.js | 17 + test/exports.js | 8 +- 9 files changed, 1705 insertions(+), 1586 deletions(-) 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/lib/express.js b/lib/express.js index 67b4f6f8d5..e3214f0393 100644 --- a/lib/express.js +++ b/lib/express.js @@ -42,21 +42,23 @@ function createApplication() { mixin(app, proto, false); // expose the prototype that will get set on requests - app.request = Object.create(req.req, { + app.request = Object.create(req, { app: { configurable: true, enumerable: true, writable: true, value: app } }) // expose the prototype that will get set on responses - app.response = Object.create(res.res, { + app.response = Object.create(res, { app: { configurable: true, enumerable: true, writable: true, value: app } }) if (isHttp2Supported) { - app.http2Request = Object.create(req.http2Req, { + 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(res.http2Res, { + app.http2Response = Object.create(http2Res, { app: { configurable: true, enumerable: true, writable: true, value: app } }); } diff --git a/lib/http2Request.js b/lib/http2Request.js new file mode 100644 index 0000000000..500a50cd6f --- /dev/null +++ b/lib/http2Request.js @@ -0,0 +1,45 @@ +/*! + * 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 http2Constants = http2.constants; +var decorator = require('./requestDecorator'); +var defineGetter = require('./utils').defineGetter; +var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); + +/** + * Override default http2 path to return only pathname + * as it returns full url + * Gets the full URL from the headers + * Creates a mock object for parse to cache on and read from + * as url in http2 refers to this.path which will results in an error + */ +defineGetter(http2Req, 'path', function path() { + var path; + var headers = this.headers; + if (headers === undefined) { + return ''; + } + path = headers[http2Constants.HTTP2_HEADER_PATH]; + this.parsedObject = this.parsedObject || {url: path}; + this.parsedObject.url = path; + return parse(this.parsedObject).pathname; +}); + +/** + * Module exports. + * @public + */ + +module.exports = http2Req; diff --git a/lib/http2Response.js b/lib/http2Response.js new file mode 100644 index 0000000000..cd2d0990ae --- /dev/null +++ b/lib/http2Response.js @@ -0,0 +1,47 @@ +/*! + * 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)); +var statusMessageWarned = false; +/** + * Add setter for statusMessage as it's deprecated, node doesn't set setter only getter + * so without setting this setter it fails + * Remove if node adds deprecation setter as well + */ +Object.defineProperty(res, 'statusMessage', { + set: function (x) { + //Empty setter for statusMessage + if (statusMessageWarned === false) { + process.emitWarning( + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)', + 'UnsupportedWarning' + ); + statusMessageWarned = true; + } + } +}); + +/** + * Module exports. + * @public + */ + +module.exports = res; + diff --git a/lib/request.js b/lib/request.js index 774ac680c9..332ab7bb6d 100644 --- a/lib/request.js +++ b/lib/request.js @@ -12,521 +12,18 @@ * Module dependencies. * @private */ - -var accepts = require('accepts'); -var deprecate = require('depd')('express'); -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'); -var isHttp2Supported = require('./utils').isHttp2Supported; + /** * Request prototype. * @public */ -var req = Object.create(http.IncomingMessage.prototype) +var req = decorator(Object.create(http.IncomingMessage.prototype)); /** * Module exports. * @public */ -module.exports = { - req: setMethods(req) -}; - -if (isHttp2Supported) { - var http2 = require('http2'); - var http2Req = Object.create(http2.Http2ServerRequest.prototype); - module.exports['http2Req'] = setMethods(http2Req); -} - -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); - }; - - req.acceptsCharset = deprecate.function(req.acceptsCharsets, - 'req.acceptsCharset: Use acceptsCharsets instead'); - - /** - * 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); - }; - - req.acceptsLanguage = deprecate.function(req.acceptsLanguages, - 'req.acceptsLanguage: Use acceptsLanguages instead'); - - /** - * 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); - }; - - /** - * Return the value of param `name` when present or `defaultValue`. - * - * - Checks route placeholders, ex: _/user/:id_ - * - Checks body params, ex: id=12, {"id":12} - * - Checks query string params, ex: ?id=12 - * - * To utilize request bodies, `req.body` - * should be an object. This can be done by using - * the `bodyParser()` middleware. - * - * @param {String} name - * @param {Mixed} [defaultValue] - * @return {String} - * @public - */ - - req.param = function param(name, defaultValue) { - var params = this.params || {}; - var body = this.body || {}; - var query = this.query || {}; - - var args = arguments.length === 1 - ? 'name' - : 'name, default'; - deprecate('req.param(' + args + '): Use req.params, req.body, or req.query instead'); - - if (null != params[name] && params.hasOwnProperty(name)) return params[name]; - if (null != body[name]) return body[name]; - if (null != query[name]) return query[name]; - - return defaultValue; - }; - - /** - * 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. - proto = this.get('X-Forwarded-Proto') || proto; - return proto.split(/\s*,\s*/)[0]; - }); - - /** - * 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`. - * Check for IncomingMessage is to avoid http2 stack overflow - * @return {String} - * @public - */ - if (req instanceof http.IncomingMessage) { - defineGetter(req, 'path', function path() { - return parse(this).pathname; - }); - } - - - /** - * 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} - * @public - */ - - defineGetter(req, 'hostname', function hostname() { - var trust = this.app.get('trust proxy fn'); - var host = this.get('X-Forwarded-Host'); - - if (!host || !trust(this.connection.remoteAddress, 0)) { - host = this.get('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; - }); - - // TODO: change req.host to return host in next major - - defineGetter(req, 'host', deprecate.function(function host() { - return this.hostname; - }, 'req.host: Use req.hostname instead')); - - /** - * 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 - }); -} +module.exports = req; diff --git a/lib/requestDecorator.js b/lib/requestDecorator.js new file mode 100644 index 0000000000..93317a9bf7 --- /dev/null +++ b/lib/requestDecorator.js @@ -0,0 +1,506 @@ +/*! + * 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 http = require('http'); +var fresh = require('fresh'); +var parseRange = require('range-parser'); +var parse = require('parseurl'); +var proxyaddr = require('proxy-addr'); +var defineGetter = require('./utils').defineGetter; + +/** + * 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); + }; + + req.acceptsCharset = deprecate.function(req.acceptsCharsets, + 'req.acceptsCharset: Use acceptsCharsets instead'); + + /** + * 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); + }; + + req.acceptsLanguage = deprecate.function(req.acceptsLanguages, + 'req.acceptsLanguage: Use acceptsLanguages instead'); + + /** + * 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); + }; + + /** + * Return the value of param `name` when present or `defaultValue`. + * + * - Checks route placeholders, ex: _/user/:id_ + * - Checks body params, ex: id=12, {"id":12} + * - Checks query string params, ex: ?id=12 + * + * To utilize request bodies, `req.body` + * should be an object. This can be done by using + * the `bodyParser()` middleware. + * + * @param {String} name + * @param {Mixed} [defaultValue] + * @return {String} + * @public + */ + + req.param = function param(name, defaultValue) { + var params = this.params || {}; + var body = this.body || {}; + var query = this.query || {}; + + var args = arguments.length === 1 + ? 'name' + : 'name, default'; + deprecate('req.param(' + args + '): Use req.params, req.body, or req.query instead'); + + if (null != params[name] && params.hasOwnProperty(name)) return params[name]; + if (null != body[name]) return body[name]; + if (null != query[name]) return query[name]; + + return defaultValue; + }; + + /** + * 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. + proto = this.get('X-Forwarded-Proto') || proto; + return proto.split(/\s*,\s*/)[0]; + }); + + /** + * 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 hostname. + * + * When the "trust proxy" setting trusts the socket + * address, the "X-Forwarded-Host" header field will + * be trusted. + * + * @return {String} + * @public + */ + + defineGetter(req, 'hostname', function hostname() { + var trust = this.app.get('trust proxy fn'); + var host = this.get('X-Forwarded-Host'); + + if (!host || !trust(this.connection.remoteAddress, 0)) { + host = this.get('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; + }); + + // TODO: change req.host to return host in next major + + defineGetter(req, 'host', deprecate.function(function host() { + return this.hostname; + }, 'req.host: Use req.hostname instead')); + + /** + * 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; +} + + diff --git a/lib/response.js b/lib/response.js index 6f3e376a32..24fd99bb02 100644 --- a/lib/response.js +++ b/lib/response.js @@ -11,1085 +11,17 @@ * Module dependencies. * @private */ - -var contentDisposition = require('content-disposition'); -var deprecate = require('depd')('express'); -var encodeUrl = require('encodeurl'); -var escapeHtml = require('escape-html'); var http = require('http'); -var isAbsolute = require('./utils').isAbsolute; -var onFinished = require('on-finished'); -var path = require('path'); -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 isHttp2Supported = require('./utils').isHttp2Supported; -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 http2 = isHttp2Supported ? require('http2') : null; +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: setMethods(res) -} - -if (isHttp2Supported) { - var http2Res = Object.create(http2.Http2ServerResponse.prototype); - module.exports['http2Res'] = setMethods(http2Res); -} - -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(new Buffer('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 len; - var req = this.req; - var type; - - // settings - var app = this.app; - - // allow status / body - if (arguments.length === 2) { - // res.send(body, status) backwards compat - if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') { - deprecate('res.send(body, status): Use res.status(status).send(body) instead'); - this.statusCode = arguments[1]; - } else { - deprecate('res.send(status, body): Use res.status(status).send(body) instead'); - this.statusCode = arguments[0]; - chunk = arguments[1]; - } - } - - // disambiguate res.send(status) and res.send(status, num) - if (typeof chunk === 'number' && arguments.length === 1) { - // res.send(status) will set status message as text string - if (!this.get('Content-Type')) { - this.type('txt'); - } - - deprecate('res.send(status): Use res.sendStatus(status) instead'); - this.statusCode = chunk; - chunk = statuses[chunk] - } - - 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')); - } - } - - // populate Content-Length - if (chunk !== undefined) { - if (!Buffer.isBuffer(chunk)) { - // convert chunk to Buffer; saves later double conversions - chunk = new Buffer(chunk, encoding); - encoding = undefined; - } - - len = chunk.length; - this.set('Content-Length', len); - } - - // populate ETag - var etag; - var generateETag = len !== undefined && app.get('etag fn'); - if (typeof generateETag === 'function' && !this.get('ETag')) { - if ((etag = generateETag(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) { - var val = obj; - - // allow status / body - if (arguments.length === 2) { - // res.json(body, status) backwards compat - if (typeof arguments[1] === 'number') { - deprecate('res.json(obj, status): Use res.status(status).json(obj) instead'); - this.statusCode = arguments[1]; - } else { - deprecate('res.json(status, obj): Use res.status(status).json(obj) instead'); - this.statusCode = arguments[0]; - val = arguments[1]; - } - } - - // settings - var app = this.app; - var replacer = app.get('json replacer'); - var spaces = app.get('json spaces'); - var body = stringify(val, replacer, spaces); - - // content-type - if (!this.get('Content-Type')) { - this.set('Content-Type', 'application/json'); - } - - return this.send(body); - }; - - if (isHttp2Supported && res instanceof http2.Http2ServerResponse) { - Object.defineProperty(res, 'statusMessage', { - set: function (x) { - //Empty setter for statusMessage - } - }); - } - - /** - * 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) { - var val = obj; - - // allow status / body - if (arguments.length === 2) { - // res.json(body, status) backwards compat - if (typeof arguments[1] === 'number') { - deprecate('res.jsonp(obj, status): Use res.status(status).json(obj) instead'); - this.statusCode = arguments[1]; - } else { - deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead'); - this.statusCode = arguments[0]; - val = arguments[1]; - } - } - - // settings - var app = this.app; - var replacer = app.get('json replacer'); - var spaces = app.get('json spaces'); - var body = stringify(val, replacer, spaces); - 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.charset = 'utf-8'; - 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 && !isAbsolute(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`. - * - * 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 (path, options, callback) { - var done = callback; - var req = this.req; - var res = this; - var next = req.next; - var opts = options || {}; - - // support function as second arg - if (typeof options === 'function') { - done = options; - opts = {}; - } - - // create file stream - var file = send(req, path, 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 !== 'ECONNABORT' && err.syscall !== 'write') { - next(err); - } - }); - }; - - res.sendfile = deprecate.function(res.sendfile, - 'res.sendfile: Use res.sendFile instead'); - - /** - * 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. - * - * This method uses `res.sendfile()`. - * - * @public - */ - - res.download = function download(path, filename, callback) { - var done = callback; - var name = filename; - - // support function as second arg - if (typeof filename === 'function') { - done = filename; - name = null; - } - - // set Content-Disposition when file is sent - var headers = { - 'Content-Disposition': contentDisposition(name || path) - }; - - // Resolve the full path for sendFile - var fullPath = resolve(path); - - return this.sendFile(fullPath, { headers: headers }, 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) { - if (typeof arguments[0] === 'number') { - status = arguments[0]; - address = arguments[1]; - } else { - deprecate('res.redirect(url, status): Use res.redirect(status, url) instead'); - status = 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) { - // checks for back-compat - if (!field || (Array.isArray(field) && !field.length)) { - deprecate('res.vary(): Provide a field name'); - return this; - } - - 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. - * @private - */ - -function stringify(value, replacer, spaces) { - // v8 checks arguments.length for optimizing simple call - // https://bugs.chromium.org/p/v8/issues/detail?id=4730 - return replacer || spaces - ? JSON.stringify(value, replacer, spaces) - : JSON.stringify(value); -} +module.exports = res; diff --git a/lib/responseDecorator.js b/lib/responseDecorator.js new file mode 100644 index 0000000000..a21c4368d4 --- /dev/null +++ b/lib/responseDecorator.js @@ -0,0 +1,1073 @@ +/*! + * 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 deprecate = require('depd')('express'); +var encodeUrl = require('encodeurl'); +var escapeHtml = require('escape-html'); +var isAbsolute = require('./utils').isAbsolute; +var onFinished = require('on-finished'); +var path = require('path'); +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(new Buffer('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 len; + var req = this.req; + var type; + + // settings + var app = this.app; + + // allow status / body + if (arguments.length === 2) { + // res.send(body, status) backwards compat + if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') { + deprecate('res.send(body, status): Use res.status(status).send(body) instead'); + this.statusCode = arguments[1]; + } else { + deprecate('res.send(status, body): Use res.status(status).send(body) instead'); + this.statusCode = arguments[0]; + chunk = arguments[1]; + } + } + + // disambiguate res.send(status) and res.send(status, num) + if (typeof chunk === 'number' && arguments.length === 1) { + // res.send(status) will set status message as text string + if (!this.get('Content-Type')) { + this.type('txt'); + } + + deprecate('res.send(status): Use res.sendStatus(status) instead'); + this.statusCode = chunk; + chunk = statuses[chunk] + } + + 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')); + } + } + + // populate Content-Length + if (chunk !== undefined) { + if (!Buffer.isBuffer(chunk)) { + // convert chunk to Buffer; saves later double conversions + chunk = new Buffer(chunk, encoding); + encoding = undefined; + } + + len = chunk.length; + this.set('Content-Length', len); + } + + // populate ETag + var etag; + var generateETag = len !== undefined && app.get('etag fn'); + if (typeof generateETag === 'function' && !this.get('ETag')) { + if ((etag = generateETag(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) { + var val = obj; + + // allow status / body + if (arguments.length === 2) { + // res.json(body, status) backwards compat + if (typeof arguments[1] === 'number') { + deprecate('res.json(obj, status): Use res.status(status).json(obj) instead'); + this.statusCode = arguments[1]; + } else { + deprecate('res.json(status, obj): Use res.status(status).json(obj) instead'); + this.statusCode = arguments[0]; + val = arguments[1]; + } + } + + // settings + var app = this.app; + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = stringify(val, replacer, spaces); + + // 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) { + var val = obj; + + // allow status / body + if (arguments.length === 2) { + // res.json(body, status) backwards compat + if (typeof arguments[1] === 'number') { + deprecate('res.jsonp(obj, status): Use res.status(status).json(obj) instead'); + this.statusCode = arguments[1]; + } else { + deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead'); + this.statusCode = arguments[0]; + val = arguments[1]; + } + } + + // settings + var app = this.app; + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = stringify(val, replacer, spaces); + 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.charset = 'utf-8'; + 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 && !isAbsolute(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`. + * + * 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 (path, options, callback) { + var done = callback; + var req = this.req; + var res = this; + var next = req.next; + var opts = options || {}; + + // support function as second arg + if (typeof options === 'function') { + done = options; + opts = {}; + } + + // create file stream + var file = send(req, path, 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 !== 'ECONNABORT' && err.syscall !== 'write') { + next(err); + } + }); + }; + + res.sendfile = deprecate.function(res.sendfile, + 'res.sendfile: Use res.sendFile instead'); + + /** + * 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. + * + * This method uses `res.sendfile()`. + * + * @public + */ + + res.download = function download(path, filename, callback) { + var done = callback; + var name = filename; + + // support function as second arg + if (typeof filename === 'function') { + done = filename; + name = null; + } + + // set Content-Disposition when file is sent + var headers = { + 'Content-Disposition': contentDisposition(name || path) + }; + + // Resolve the full path for sendFile + var fullPath = resolve(path); + + return this.sendFile(fullPath, { headers: headers }, 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) { + if (typeof arguments[0] === 'number') { + status = arguments[0]; + address = arguments[1]; + } else { + deprecate('res.redirect(url, status): Use res.redirect(status, url) instead'); + status = 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) { + // checks for back-compat + if (!field || (Array.isArray(field) && !field.length)) { + deprecate('res.vary(): Provide a field name'); + return this; + } + + 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. + * @private + */ + +function stringify(value, replacer, spaces) { + // v8 checks arguments.length for optimizing simple call + // https://bugs.chromium.org/p/v8/issues/detail?id=4730 + return replacer || spaces + ? JSON.stringify(value, replacer, spaces) + : JSON.stringify(value); +} diff --git a/lib/utils.js b/lib/utils.js index 679c0f1298..063e063c56 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -287,6 +287,23 @@ exports.setCharset = function setCharset(type, charset) { return contentType.format(parsed); }; +/** + * Helper function for creating a getter on an object. + * + * @param {Object} obj + * @param {String} name + * @param {Function} getter + * @private + */ + +exports.defineGetter = function defineGetter(obj, name, getter) { + Object.defineProperty(obj, name, { + configurable: true, + enumerable: true, + get: getter + }); +} + /** * Parse an extended query string with qs. * diff --git a/test/exports.js b/test/exports.js index 5047c78ca2..2a80eedbbe 100644 --- a/test/exports.js +++ b/test/exports.js @@ -13,11 +13,11 @@ describe('exports', function(){ }) it('should expose the request prototype', function(){ - express.request.req.accepts.should.be.a.Function() + express.request.accepts.should.be.a.Function() }) it('should expose the response prototype', function(){ - express.response.res.send.should.be.a.Function() + express.response.send.should.be.a.Function() }) it('should permit modifying the .application prototype', function(){ @@ -26,7 +26,7 @@ describe('exports', function(){ }) it('should permit modifying the .request prototype', function(done){ - express.request.req.foo = function(){ return 'bar'; }; + express.request.foo = function(){ return 'bar'; }; var app = express(); app.use(function(req, res, next){ @@ -39,7 +39,7 @@ describe('exports', function(){ }) it('should permit modifying the .response prototype', function(done){ - express.response.res.foo = function(){ this.send('bar'); }; + express.response.foo = function(){ this.send('bar'); }; var app = express(); app.use(function(req, res, next){ From f98568f688ef917a30e0aedc11b2c5f1cc64bcff Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Fri, 1 Sep 2017 12:24:22 +0300 Subject: [PATCH 03/12] Require parseurl and don't emit warning on setter --- lib/http2Request.js | 2 +- lib/http2Response.js | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/http2Request.js b/lib/http2Request.js index 500a50cd6f..1be8efc2ee 100644 --- a/lib/http2Request.js +++ b/lib/http2Request.js @@ -17,7 +17,7 @@ var http2Constants = http2.constants; var decorator = require('./requestDecorator'); var defineGetter = require('./utils').defineGetter; var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); - +var parse = require('parseurl'); /** * Override default http2 path to return only pathname * as it returns full url diff --git a/lib/http2Response.js b/lib/http2Response.js index cd2d0990ae..439a85dbfd 100644 --- a/lib/http2Response.js +++ b/lib/http2Response.js @@ -19,7 +19,7 @@ var decorator = require('./responseDecorator'); */ var res = decorator(Object.create(Http2ServerResponse.prototype)); -var statusMessageWarned = false; + /** * Add setter for statusMessage as it's deprecated, node doesn't set setter only getter * so without setting this setter it fails @@ -27,14 +27,7 @@ var statusMessageWarned = false; */ Object.defineProperty(res, 'statusMessage', { set: function (x) { - //Empty setter for statusMessage - if (statusMessageWarned === false) { - process.emitWarning( - 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)', - 'UnsupportedWarning' - ); - statusMessageWarned = true; - } + //Empty setter for statusMessage } }); From 17efcbfc40f449824f2ee746fb7fa0c1fa6fc40b Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Fri, 1 Sep 2017 12:31:05 +0300 Subject: [PATCH 04/12] Lint --- lib/http2Request.js | 2 +- lib/http2Response.js | 2 +- lib/requestDecorator.js | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/http2Request.js b/lib/http2Request.js index 1be8efc2ee..09c2523669 100644 --- a/lib/http2Request.js +++ b/lib/http2Request.js @@ -19,7 +19,7 @@ var defineGetter = require('./utils').defineGetter; var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); var parse = require('parseurl'); /** - * Override default http2 path to return only pathname + * Override default http2 path to return only pathname * as it returns full url * Gets the full URL from the headers * Creates a mock object for parse to cache on and read from diff --git a/lib/http2Response.js b/lib/http2Response.js index 439a85dbfd..49d4f52d0d 100644 --- a/lib/http2Response.js +++ b/lib/http2Response.js @@ -27,7 +27,7 @@ var res = decorator(Object.create(Http2ServerResponse.prototype)); */ Object.defineProperty(res, 'statusMessage', { set: function (x) { - //Empty setter for statusMessage + //Empty setter for statusMessage } }); diff --git a/lib/requestDecorator.js b/lib/requestDecorator.js index 93317a9bf7..b987140e5f 100644 --- a/lib/requestDecorator.js +++ b/lib/requestDecorator.js @@ -17,7 +17,6 @@ var accepts = require('accepts'); var deprecate = require('depd')('express'); var isIP = require('net').isIP; var typeis = require('type-is'); -var http = require('http'); var fresh = require('fresh'); var parseRange = require('range-parser'); var parse = require('parseurl'); @@ -399,7 +398,6 @@ function setMethods(req) { return subdomains.slice(offset); }); - /** * Short-hand for `url.parse(req.url).pathname`. * @return {String} @@ -502,5 +500,3 @@ function setMethods(req) { return req; } - - From b9276c7fc130b77f12bbb387317cce9db26a4859 Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Fri, 1 Sep 2017 14:18:01 +0300 Subject: [PATCH 05/12] Http2 Example --- examples/http2/index.js | 39 +++++++++++++++++++++++++++++++ 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 +++++++ 8 files changed, 97 insertions(+) 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 diff --git a/examples/http2/index.js b/examples/http2/index.js new file mode 100644 index 0000000000..adcae9a53d --- /dev/null +++ b/examples/http2/index.js @@ -0,0 +1,39 @@ +var isHttp2Supported = require('../../lib/utils').isHttp2Supported; +if (!isHttp2Supported) { + return; +} +var fs = require('fs'); +var path = require('path'); +var express = require('../../'); +var http2 = require('http2'); +var keys = path.join(__dirname, 'keys'); +var app = express(); +var style = fs.readFileSync('./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.get('/', function(req, res, next){ + pushStyle(res); + next(); +}); +app.get('/index.html', function(req, res, next) { + pushStyle(res); + next(); +}) +app.use(express.static('static')); + +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..1aa9801577 --- /dev/null +++ b/examples/http2/static/s1.js @@ -0,0 +1 @@ +console.log('Loaded'); \ No newline at end of file diff --git a/examples/http2/static/s2.js b/examples/http2/static/s2.js new file mode 100644 index 0000000000..1aa9801577 --- /dev/null +++ b/examples/http2/static/s2.js @@ -0,0 +1 @@ +console.log('Loaded'); \ No newline at end of file diff --git a/examples/http2/static/s3.js b/examples/http2/static/s3.js new file mode 100644 index 0000000000..1aa9801577 --- /dev/null +++ b/examples/http2/static/s3.js @@ -0,0 +1 @@ +console.log('Loaded'); \ No newline at end of file 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 From 2e1e78b0dedacb2fcb073ebc81d08681853c08ac Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Fri, 1 Sep 2017 19:52:09 +0300 Subject: [PATCH 06/12] Lint... --- examples/http2/index.js | 67 +++++++++++++++++++------------------ examples/http2/static/s1.js | 2 +- examples/http2/static/s2.js | 2 +- examples/http2/static/s3.js | 2 +- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/examples/http2/index.js b/examples/http2/index.js index adcae9a53d..9412f8d638 100644 --- a/examples/http2/index.js +++ b/examples/http2/index.js @@ -1,39 +1,40 @@ var isHttp2Supported = require('../../lib/utils').isHttp2Supported; -if (!isHttp2Supported) { - return; -} -var fs = require('fs'); -var path = require('path'); -var express = require('../../'); -var http2 = require('http2'); -var keys = path.join(__dirname, 'keys'); -var app = express(); -var style = fs.readFileSync('./static/style.css', 'utf8'); -function pushStyle(res) { - res.createPushResponse({':path': '/style.css', - ':status': 200, - 'content-type': 'text/css'}, function(err, newResponse) { +if (isHttp2Supported) { + var fs = require('fs'); + var path = require('path'); + var express = require('../../'); + var http2 = require('http2'); + var keys = path.join(__dirname, 'keys'); + var app = express(); + var style = fs.readFileSync('./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.get('/', function(req, res, next){ - pushStyle(res); - next(); -}); -app.get('/index.html', function(req, res, next) { - pushStyle(res); - next(); -}) -app.use(express.static('static')); + app.get('/', function(req, res, next){ + pushStyle(res); + next(); + }); + app.get('/index.html', function(req, res, next) { + pushStyle(res); + next(); + }) + app.use(express.static('static')); -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'); + 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/static/s1.js b/examples/http2/static/s1.js index 1aa9801577..48febb422d 100644 --- a/examples/http2/static/s1.js +++ b/examples/http2/static/s1.js @@ -1 +1 @@ -console.log('Loaded'); \ No newline at end of file +console.log('Loaded'); diff --git a/examples/http2/static/s2.js b/examples/http2/static/s2.js index 1aa9801577..48febb422d 100644 --- a/examples/http2/static/s2.js +++ b/examples/http2/static/s2.js @@ -1 +1 @@ -console.log('Loaded'); \ No newline at end of file +console.log('Loaded'); diff --git a/examples/http2/static/s3.js b/examples/http2/static/s3.js index 1aa9801577..48febb422d 100644 --- a/examples/http2/static/s3.js +++ b/examples/http2/static/s3.js @@ -1 +1 @@ -console.log('Loaded'); \ No newline at end of file +console.log('Loaded'); From 878bf5fad9e5aff75580c95fa4d0bad67c5b4174 Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Sun, 3 Sep 2017 23:36:21 +0300 Subject: [PATCH 07/12] Fix example --- examples/http2/index.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/http2/index.js b/examples/http2/index.js index 9412f8d638..051846635f 100644 --- a/examples/http2/index.js +++ b/examples/http2/index.js @@ -1,12 +1,11 @@ -var isHttp2Supported = require('../../lib/utils').isHttp2Supported; -if (isHttp2Supported) { +try { + var http2 = require('http2'); var fs = require('fs'); var path = require('path'); var express = require('../../'); - var http2 = require('http2'); var keys = path.join(__dirname, 'keys'); var app = express(); - var style = fs.readFileSync('./static/style.css', 'utf8'); + var style = fs.readFileSync(path.resolve(__dirname, './static/style.css'), 'utf8'); function pushStyle(res) { res.createPushResponse({ ':path': '/style.css', @@ -18,23 +17,22 @@ if (isHttp2Supported) { }); } - app.get('/', function(req, res, next){ - pushStyle(res); - next(); - }); - app.get('/index.html', function(req, res, next) { - pushStyle(res); - next(); - }) - app.use(express.static('static')); + 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'); } +} catch (_) { + console.log('Http2 is not supported'); } From e0552bd29c682a68c253d60a6dea230bfcce9860 Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Mon, 4 Sep 2017 00:09:00 +0300 Subject: [PATCH 08/12] Remove try/catch in example Better override for path/url - use same mechanism in http1, make sure to bring full path for url --- examples/http2/index.js | 64 +++++++++++++++++++---------------------- lib/http2Request.js | 9 ++---- 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/examples/http2/index.js b/examples/http2/index.js index 051846635f..842996a7ac 100644 --- a/examples/http2/index.js +++ b/examples/http2/index.js @@ -1,38 +1,34 @@ -try { - 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); - }); - } +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); - } - }})); +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); +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'); - } -} catch (_) { - console.log('Http2 is not supported'); +/* istanbul ignore next */ +if (!module.parent) { + server.listen(3000); + console.log('Express started on port 3000'); } diff --git a/lib/http2Request.js b/lib/http2Request.js index 09c2523669..f72be4ef12 100644 --- a/lib/http2Request.js +++ b/lib/http2Request.js @@ -17,7 +17,6 @@ var http2Constants = http2.constants; var decorator = require('./requestDecorator'); var defineGetter = require('./utils').defineGetter; var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); -var parse = require('parseurl'); /** * Override default http2 path to return only pathname * as it returns full url @@ -25,16 +24,12 @@ var parse = require('parseurl'); * Creates a mock object for parse to cache on and read from * as url in http2 refers to this.path which will results in an error */ -defineGetter(http2Req, 'path', function path() { - var path; +defineGetter(http2Req, 'url', function path() { var headers = this.headers; if (headers === undefined) { return ''; } - path = headers[http2Constants.HTTP2_HEADER_PATH]; - this.parsedObject = this.parsedObject || {url: path}; - this.parsedObject.url = path; - return parse(this.parsedObject).pathname; + return headers[http2Constants.HTTP2_HEADER_PATH]; }); /** From d3620d248d23ffabd5d514e33bdffab4ec6234c5 Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Mon, 4 Sep 2017 00:37:12 +0300 Subject: [PATCH 09/12] Remove defineGetter from utils --- lib/http2Request.js | 21 ++++++++++++--------- lib/requestDecorator.js | 18 +++++++++++++++++- lib/utils.js | 17 ----------------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/lib/http2Request.js b/lib/http2Request.js index f72be4ef12..b56fe097f8 100644 --- a/lib/http2Request.js +++ b/lib/http2Request.js @@ -15,7 +15,6 @@ var http2 = require('http2'); var http2Constants = http2.constants; var decorator = require('./requestDecorator'); -var defineGetter = require('./utils').defineGetter; var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); /** * Override default http2 path to return only pathname @@ -24,14 +23,18 @@ var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); * Creates a mock object for parse to cache on and read from * as url in http2 refers to this.path which will results in an error */ -defineGetter(http2Req, 'url', function path() { - var headers = this.headers; - if (headers === undefined) { - return ''; - } - return headers[http2Constants.HTTP2_HEADER_PATH]; -}); - +Object.defineProperty(http2Req, 'url', + { + get: function path() { + var headers = this.headers; + if (headers === undefined) { + return ''; + } + return headers[http2Constants.HTTP2_HEADER_PATH]; + }, + configurable: true, + enumerable: true + }); /** * Module exports. * @public diff --git a/lib/requestDecorator.js b/lib/requestDecorator.js index b987140e5f..7619ec802e 100644 --- a/lib/requestDecorator.js +++ b/lib/requestDecorator.js @@ -21,7 +21,6 @@ var fresh = require('fresh'); var parseRange = require('range-parser'); var parse = require('parseurl'); var proxyaddr = require('proxy-addr'); -var defineGetter = require('./utils').defineGetter; /** * Request prototype. @@ -500,3 +499,20 @@ function setMethods(req) { 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/utils.js b/lib/utils.js index 063e063c56..679c0f1298 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -287,23 +287,6 @@ exports.setCharset = function setCharset(type, charset) { return contentType.format(parsed); }; -/** - * Helper function for creating a getter on an object. - * - * @param {Object} obj - * @param {String} name - * @param {Function} getter - * @private - */ - -exports.defineGetter = function defineGetter(obj, name, getter) { - Object.defineProperty(obj, name, { - configurable: true, - enumerable: true, - get: getter - }); -} - /** * Parse an extended query string with qs. * From e424883c14f99f7587d6fbef7a5d33a2e0135e52 Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Mon, 4 Sep 2017 00:44:02 +0300 Subject: [PATCH 10/12] Rename function name --- lib/http2Request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/http2Request.js b/lib/http2Request.js index b56fe097f8..987ab2d529 100644 --- a/lib/http2Request.js +++ b/lib/http2Request.js @@ -25,7 +25,7 @@ var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); */ Object.defineProperty(http2Req, 'url', { - get: function path() { + get: function url() { var headers = this.headers; if (headers === undefined) { return ''; From be2aed1846e243987c3f83fe73b9221f5c3e25db Mon Sep 17 00:00:00 2001 From: Pini Date: Tue, 5 Sep 2017 16:43:00 +0300 Subject: [PATCH 11/12] Remove overridding setter for statusMessage - will land in Node soon --- lib/http2Response.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/http2Response.js b/lib/http2Response.js index 49d4f52d0d..27dcd7b3d2 100644 --- a/lib/http2Response.js +++ b/lib/http2Response.js @@ -20,17 +20,6 @@ var decorator = require('./responseDecorator'); var res = decorator(Object.create(Http2ServerResponse.prototype)); -/** - * Add setter for statusMessage as it's deprecated, node doesn't set setter only getter - * so without setting this setter it fails - * Remove if node adds deprecation setter as well - */ -Object.defineProperty(res, 'statusMessage', { - set: function (x) { - //Empty setter for statusMessage - } -}); - /** * Module exports. * @public From 2ac75f2ffa75bafc05e53b90d3bde8b02b6028ce Mon Sep 17 00:00:00 2001 From: Pini Houri Date: Tue, 12 Sep 2017 22:19:04 +0300 Subject: [PATCH 12/12] Remove unneeded decorators after node js fixes --- lib/http2Request.js | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/lib/http2Request.js b/lib/http2Request.js index 987ab2d529..733c095ba7 100644 --- a/lib/http2Request.js +++ b/lib/http2Request.js @@ -13,28 +13,8 @@ * @private */ var http2 = require('http2'); -var http2Constants = http2.constants; var decorator = require('./requestDecorator'); var http2Req = decorator(Object.create(http2.Http2ServerRequest.prototype)); -/** - * Override default http2 path to return only pathname - * as it returns full url - * Gets the full URL from the headers - * Creates a mock object for parse to cache on and read from - * as url in http2 refers to this.path which will results in an error - */ -Object.defineProperty(http2Req, 'url', - { - get: function url() { - var headers = this.headers; - if (headers === undefined) { - return ''; - } - return headers[http2Constants.HTTP2_HEADER_PATH]; - }, - configurable: true, - enumerable: true - }); /** * Module exports. * @public