diff --git a/API.md b/API.md index 66279dea1..fc9b3860b 100755 --- a/API.md +++ b/API.md @@ -2973,21 +2973,6 @@ string payload or escaping it after stringification. Supports the following: - `escape` - calls [`Hoek.jsonEscape()`](https://hapi.dev/family/hoek/api/#escapejsonstring) after conversion to JSON string. Defaults to `false`. -### `route.options.jsonp` - -Default value: none. - -Enables JSONP support by setting the value to the query parameter name containing the function name -used to wrap the response payload. - -For example, if the value is `'callback'`, a request comes in with `'callback=me'`, and the JSON -response is `'{ "a":"b" }'`, the payload will be `'me({ "a":"b" });'`. Cannot be used with stream -responses. - -The 'Content-Type' response header is set to `'text/javascript'` and the 'X-Content-Type-Options' -response header is set to `'nosniff'`, and will override those headers even if explicitly set by -[`response.type()`](#response.type()). - ### `route.options.log` Default value: `{ collect: false }`. @@ -3615,19 +3600,12 @@ the same. The following is the complete list of steps a request can go through: - [`request.route`](#request.route) is unassigned. - [`request.url`](#request.url) can be `null` if the incoming request path is invalid. - [`request.path`](#request.path) can be an invalid path. - - JSONP configuration is ignored for any response returned from the extension point since no - route is matched yet and the JSONP configuration is unavailable. - _**Route lookup**_ - lookup based on `request.path` and `request.method`. - skips to _**onPreResponse**_ if no route is found or if the path violates the HTTP specification. -- _**JSONP processing**_ - - based on the route [`jsonp`](#route.options.jsonp) option. - - parses JSONP parameter from [`request.query`](#request.query). - - skips to _**Response validation**_ on error. - - _**Cookies processing**_ - based on the route [`state`](#route.options.state) option. - error handling based on [`failAction`](#route.options.state.failAction). @@ -3662,10 +3640,6 @@ the same. The following is the complete list of steps a request can go through: - based on the route [`validate.params`](#route.options.validate.params) option. - error handling based on [`failAction`](#route.options.validate.failAction). -- _**JSONP cleanup**_ - - based on the route [`jsonp`](#route.options.jsonp) option. - - remove the JSONP parameter from [`request.query`](#request.query). - - _**Query validation**_ - based on the route [`validate.query`](#route.options.validate.query) option. - error handling based on [`failAction`](#route.options.validate.failAction). diff --git a/lib/config.js b/lib/config.js index 5279a187d..94ca0586f 100755 --- a/lib/config.js +++ b/lib/config.js @@ -134,7 +134,6 @@ internals.routeBase = Validate.object({ escape: Validate.boolean().default(false) }) .default(), - jsonp: Validate.string(), log: Validate.object({ collect: Validate.boolean().default(false) }) diff --git a/lib/headers.js b/lib/headers.js index 8e3a86654..bdb2a3833 100755 --- a/lib/headers.js +++ b/lib/headers.js @@ -40,17 +40,7 @@ exports.content = async function (response) { await response._marshal(); - if (request.jsonp && - response._payload.jsonp) { - - response._header('content-type', 'text/javascript' + (response.settings.charset ? '; charset=' + response.settings.charset : '')); - response._header('x-content-type-options', 'nosniff'); - response._payload.jsonp(request.jsonp); - } - - if (response._payload.size && - typeof response._payload.size === 'function') { - + if (typeof response._payload.size === 'function') { response._header('content-length', response._payload.size(), { override: false }); } diff --git a/lib/request.js b/lib/request.js index 7ae525fc5..f0e6f42fe 100755 --- a/lib/request.js +++ b/lib/request.js @@ -15,7 +15,7 @@ const Transmit = require('./transmit'); const internals = { events: Podium.validate(['finish', { name: 'peek', spread: true }, 'disconnect']), - reserved: ['server', 'url', 'query', 'path', 'method', 'mime', 'setUrl', 'setMethod', 'headers', 'id', 'app', 'plugins', 'route', 'auth', 'pre', 'preResponses', 'info', 'isInjected', 'orig', 'params', 'paramsArray', 'payload', 'state', 'jsonp', 'response', 'raw', 'domain', 'log', 'logs', 'generateResponse'] + reserved: ['server', 'url', 'query', 'path', 'method', 'mime', 'setUrl', 'setMethod', 'headers', 'id', 'app', 'plugins', 'route', 'auth', 'pre', 'preResponses', 'info', 'isInjected', 'orig', 'params', 'paramsArray', 'payload', 'state', 'response', 'raw', 'domain', 'log', 'logs', 'generateResponse'] }; @@ -41,7 +41,6 @@ exports = module.exports = internals.Request = class { this.app = options.app ? Object.assign({}, options.app) : {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins (shallow cloned) this.headers = req.headers; - this.jsonp = null; this.logs = []; this.method = req.method.toLowerCase(); this.mime = null; diff --git a/lib/response.js b/lib/response.js index 6bb8044c1..8840c661b 100755 --- a/lib/response.js +++ b/lib/response.js @@ -711,68 +711,33 @@ internals.Response.Payload = class extends Stream.Readable { super(); this._data = payload; - this._prefix = null; - this._suffix = null; - this._sizeOffset = 0; this._encoding = options.encoding; } _read(size) { - if (this._prefix) { - this.push(this._prefix, this._encoding); - } - if (this._data) { this.push(this._data, this._encoding); } - if (this._suffix) { - this.push(this._suffix, this._encoding); - } - this.push(null); } size() { if (!this._data) { - return this._sizeOffset; + return 0; } - return (Buffer.isBuffer(this._data) ? this._data.length : Buffer.byteLength(this._data, this._encoding)) + this._sizeOffset; - } - - jsonp(variable) { - - this._sizeOffset = this._sizeOffset + variable.length + 7; - this._prefix = '/**/' + variable + '('; // '/**/' prefix prevents CVE-2014-4671 security exploit - - if (this._data !== null && - !Buffer.isBuffer(this._data)) { - - this._data = this._data - .replace(/\u2028/g, '\\u2028') - .replace(/\u2029/g, '\\u2029'); - } - - this._suffix = ');'; + return Buffer.isBuffer(this._data) ? this._data.length : Buffer.byteLength(this._data, this._encoding); } writeToStream(stream) { - if (this._prefix) { - stream.write(this._prefix, this._encoding); - } - if (this._data) { stream.write(this._data, this._encoding); } - if (this._suffix) { - stream.write(this._suffix, this._encoding); - } - stream.end(); } }; diff --git a/lib/route.js b/lib/route.js index 0e6a70dcd..6feca6011 100755 --- a/lib/route.js +++ b/lib/route.js @@ -2,7 +2,6 @@ const Assert = require('assert'); -const Boom = require('@hapi/boom'); const Bounce = require('@hapi/bounce'); const Catbox = require('@hapi/catbox'); const Hoek = require('@hapi/hoek'); @@ -127,7 +126,6 @@ exports = module.exports = internals.Route = class { this._assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled'); this._assert(!this.settings.validate.state || this.settings.state.parse, 'Route state must be set to \'parse\' when state validation enabled'); - this._assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name'); // Authentication configuration @@ -232,10 +230,6 @@ exports = module.exports = internals.Route = class { // 'onRequest' - if (this.settings.jsonp) { - this._cycle.push(internals.parseJSONP); - } - if (this.settings.state.parse) { this._cycle.push(internals.state); } @@ -278,10 +272,6 @@ exports = module.exports = internals.Route = class { this._cycle.push(Validation.params); } - if (this.settings.jsonp) { - this._cycle.push(internals.cleanupJSONP); - } - if (this.settings.validate.query) { this._cycle.push(Validation.query); } @@ -457,30 +447,6 @@ internals.drain = async function (request) { }; -internals.jsonpRegex = /^[\w\$\[\]\.]+$/; - - -internals.parseJSONP = function (request) { - - const jsonp = request.query[request.route.settings.jsonp]; - if (jsonp) { - if (internals.jsonpRegex.test(jsonp) === false) { - throw Boom.badRequest('Invalid JSONP parameter value'); - } - - request.jsonp = jsonp; - } -}; - - -internals.cleanupJSONP = function (request) { - - if (request.jsonp) { - delete request.query[request.route.settings.jsonp]; - } -}; - - internals.config = function (chain) { if (!chain.length) { diff --git a/test/headers.js b/test/headers.js index 70edba394..c6a447649 100755 --- a/test/headers.js +++ b/test/headers.js @@ -6,7 +6,6 @@ const Code = require('@hapi/code'); const Hapi = require('..'); const Inert = require('@hapi/inert'); const Lab = require('@hapi/lab'); -const Wreck = require('@hapi/wreck'); const internals = {}; @@ -513,17 +512,6 @@ describe('Headers', () => { expect(res.headers['content-type']).to.equal('text/html'); }); - it('returns a normal response when JSONP requested but stream returned', async () => { - - const server = Hapi.server(); - const stream = Wreck.toReadableStream('test'); - stream.size = 4; // Non function for coverage - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => stream } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('test'); - }); - it('does not set content-type by default on 204 response', async () => { const server = Hapi.server(); diff --git a/test/response.js b/test/response.js index f0757fba6..28a3fb303 100755 --- a/test/response.js +++ b/test/response.js @@ -1553,16 +1553,4 @@ describe('Response', () => { await finish; }); }); - - describe('Payload', () => { - - it('streams empty string', async () => { - - const server = Hapi.server({ compression: { minBytes: 1 } }); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => '' } }); - - const res = await server.inject({ url: '/?callback=me', headers: { 'Accept-Encoding': 'gzip' } }); - expect(res.statusCode).to.equal(200); - }); - }); }); diff --git a/test/transmit.js b/test/transmit.js index 4109cc013..8449bca89 100755 --- a/test/transmit.js +++ b/test/transmit.js @@ -362,138 +362,6 @@ describe('transmission', () => { expect(res.statusCode).to.equal(201); }); - it('returns an JSONP response', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me({"some":"value"});'); - expect(res.headers['content-length']).to.equal(25); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - }); - - it('returns an JSONP response with no payload', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => null } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me();'); - expect(res.headers['content-length']).to.equal(9); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - }); - - it('returns an JSONP response (no charset)', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: (request, h) => h.response({ some: 'value' }).charset('') } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me({"some":"value"});'); - expect(res.headers['content-length']).to.equal(25); - expect(res.headers['content-type']).to.equal('text/javascript'); - }); - - it('returns a X-Content-Type-Options: nosniff header on JSONP responses', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me({"some":"value"});'); - expect(res.headers['x-content-type-options']).to.equal('nosniff'); - }); - - it('returns a normal response when JSONP enabled but not requested', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } }); - - const res = await server.inject('/'); - expect(res.payload).to.equal('{"some":"value"}'); - }); - - it('returns an JSONP response with compression', async () => { - - const server = Hapi.server({ compression: { minBytes: 1 } }); - server.route({ - method: 'GET', - path: '/user/{name*2}', - options: { - handler: (request) => { - - const parts = request.params.name.split('/'); - return { first: parts[0], last: parts[1] }; - }, - jsonp: 'callback' - } - }); - - const res = await server.inject({ url: '/user/1/2?callback=docall', headers: { 'accept-encoding': 'gzip' } }); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - expect(res.headers['content-encoding']).to.equal('gzip'); - expect(res.headers.vary).to.equal('accept-encoding'); - - const uncompressed = await internals.uncompress('unzip', res.rawPayload); - expect(uncompressed.toString()).to.equal('/**/docall({"first":"1","last":"2"});'); - }); - - it('returns an JSONP response when response is a buffer', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => Buffer.from('value') } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me(value);'); - expect(res.headers['content-length']).to.equal(14); - }); - - it('returns response on bad JSONP parameter', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } }); - - const res = await server.inject('/?callback=me*'); - expect(res.result).to.exist(); - expect(res.result.message).to.equal('Invalid JSONP parameter value'); - }); - - it('returns an JSONP handler error', async () => { - - const handler = () => { - - throw Boom.badRequest('wrong'); - }; - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler } }); - - const res = await server.inject('/?callback=me'); - expect(res.payload).to.equal('/**/me({"statusCode":400,"error":"Bad Request","message":"wrong"});'); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - }); - - it('returns an JSONP state error', async () => { - - const server = Hapi.server(); - server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => 'ok' } }); - - let validState = false; - const preResponse = (request, h) => { - - validState = request.state && typeof request.state === 'object'; - return h.continue; - }; - - server.ext('onPreResponse', preResponse); - - const res = await server.inject({ method: 'GET', url: '/?callback=me', headers: { cookie: '+' } }); - expect(res.payload).to.equal('/**/me({"statusCode":400,"error":"Bad Request","message":"Invalid cookie header"});'); - expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8'); - expect(validState).to.equal(true); - }); - it('sets specific caching headers', async () => { const server = Hapi.server();