diff --git a/examples/validation.js b/examples/validation.js index 59024540d..db1c667d5 100755 --- a/examples/validation.js +++ b/examples/validation.js @@ -59,10 +59,10 @@ internals.main(); Try various URLs like: http://localhost:8080/ // success http://localhost:8080/?username=test // success - http://localhost:8080/admin?username=walmart&password=worldofwalmartlabs // success - http://localhost:8080/admin?username=walmart // fail - http://localhost:8080/users?email=vnguyen@walmart.com // success - http://localhost:8080/users?email=@walmart.com // fail + http://localhost:8080/admin?username=steve&password=shhhhhh // success + http://localhost:8080/admin?username=steve // fail + http://localhost:8080/users?email=steve@example.com // success + http://localhost:8080/users?email=@example.com // fail http://localhost:8080/config?choices=1&choices=2 // success http://localhost:8080/config?choices=1 // success http://localhost:8080/config // fail diff --git a/examples/views/cms/views/partials/footer.html b/examples/views/cms/views/partials/footer.html index a5f072b50..5f4351815 100755 --- a/examples/views/cms/views/partials/footer.html +++ b/examples/views/cms/views/partials/footer.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/lib/auth/basic.js b/lib/auth/basic.js index ea6b0fa88..9681d8568 100755 --- a/lib/auth/basic.js +++ b/lib/auth/basic.js @@ -2,7 +2,7 @@ var Cryptiles = require('cryptiles'); var Utils = require('../utils'); -var Err = require('../error'); +var Boom = require('boom'); // Declare internals @@ -33,7 +33,7 @@ internals.Scheme.prototype.authenticate = function (request, callback) { var req = request.raw.req; var authorization = req.headers.authorization; if (!authorization) { - return callback(Err.unauthorized(null, 'Basic')); + return callback(Boom.unauthorized(null, 'Basic')); } var parts = authorization.split(/\s+/); @@ -41,16 +41,16 @@ internals.Scheme.prototype.authenticate = function (request, callback) { if (parts[0] && parts[0].toLowerCase() !== 'basic') { - return callback(Err.unauthorized(null, 'Basic')); + return callback(Boom.unauthorized(null, 'Basic')); } if (parts.length !== 2) { - return callback(Err.badRequest('Bad HTTP authentication header format', 'Basic')); + return callback(Boom.badRequest('Bad HTTP authentication header format', 'Basic')); } var credentialsParts = new Buffer(parts[1], 'base64').toString().split(':'); if (credentialsParts.length !== 2) { - return callback(Err.badRequest('Bad header internal syntax', 'Basic')); + return callback(Boom.badRequest('Bad header internal syntax', 'Basic')); } var credentials = { @@ -67,7 +67,7 @@ internals.Scheme.prototype.authenticate = function (request, callback) { if (!user) { request.log(['auth', 'error', 'user', 'unknown']); - return callback(Err.unauthorized('Bad username or password', 'Basic'), null, true); + return callback(Boom.unauthorized('Bad username or password', 'Basic'), null, true); } if (!user.hasOwnProperty('password') || @@ -75,7 +75,7 @@ internals.Scheme.prototype.authenticate = function (request, callback) { user.id !== credentials.username) { request.log(['auth', 'error', 'user', 'invalid']); - return callback(Err.internal('Bad user object received for Basic auth validation'), null, true); + return callback(Boom.internal('Bad user object received for Basic auth validation'), null, true); } if (typeof self.settings.hashPasswordFunc === 'function') { @@ -86,7 +86,7 @@ internals.Scheme.prototype.authenticate = function (request, callback) { if (!Cryptiles.fixedTimeComparison(user.password, credentials.password)) { request.log(['auth', 'error', 'user', 'password']); - return callback(Err.unauthorized('Bad username or password', 'Basic'), null, true); + return callback(Boom.unauthorized('Bad username or password', 'Basic'), null, true); } // Authenticated diff --git a/lib/auth/cookie.js b/lib/auth/cookie.js index a4b798e29..d5397189a 100755 --- a/lib/auth/cookie.js +++ b/lib/auth/cookie.js @@ -1,7 +1,7 @@ // Load modules var Utils = require('../utils'); -var Err = require('../error'); +var Boom = require('boom'); var Redirection = require('../response/redirection'); @@ -26,7 +26,8 @@ exports = module.exports = internals.Scheme = function (server, options) { var cookieOptions = { encoding: 'iron', password: this.settings.password, - isSecure: !this.settings.allowInsecure + isSecure: !this.settings.allowInsecure, + path: '/' }; if (this.settings.ttl) { @@ -49,25 +50,13 @@ internals.Scheme.prototype.authenticate = function (request, callback) { var self = this; - // Decorate request - - request.setSession = function (session) { - - request.setState(self.settings.cookie, session); - }; - - request.clearSession = function () { - - request.clearState(self.settings.cookie); - }; - var validate = function () { // Check cookie var session = request.state[self.settings.cookie]; if (!session) { - return unauthenticated(Err.unauthorized()); + return unauthenticated(Boom.unauthorized()); } self.settings.validateFunc(session, function (err, override) { @@ -78,7 +67,7 @@ internals.Scheme.prototype.authenticate = function (request, callback) { } request.log(['auth', 'validate'], err); - return unauthenticated(Err.unauthorized('Invalid cookie'), session, true); + return unauthenticated(Boom.unauthorized('Invalid cookie'), session, true); } if (override) { @@ -89,10 +78,10 @@ internals.Scheme.prototype.authenticate = function (request, callback) { }); }; - var unauthenticated = function (err) { + var unauthenticated = function (err, session, wasLogged) { if (!self.settings.redirectTo) { - return callback(err); + return callback(err, session, wasLogged); } var uri = self.settings.redirectTo; @@ -107,10 +96,26 @@ internals.Scheme.prototype.authenticate = function (request, callback) { uri += self.settings.appendNext + '=' + encodeURIComponent(request.url.path); } - return callback(new Redirection(uri)); + return callback(new Redirection(uri), session, wasLogged); }; validate(); }; +internals.Scheme.prototype.extend = function (request) { + + var self = this; + + // Decorate request + + request.setSession = function (session) { + + request.setState(self.settings.cookie, session); + }; + + request.clearSession = function () { + + request.clearState(self.settings.cookie); + }; +}; diff --git a/lib/auth/hawk.js b/lib/auth/hawk.js index 0036eec02..8a5b42968 100755 --- a/lib/auth/hawk.js +++ b/lib/auth/hawk.js @@ -2,7 +2,7 @@ var Hawk = require('hawk'); var Utils = require('../utils'); -var Err = require('../error'); +var Boom = require('boom'); // Declare internals diff --git a/lib/auth/index.js b/lib/auth/index.js index 619fc259c..3fe3fa510 100755 --- a/lib/auth/index.js +++ b/lib/auth/index.js @@ -5,7 +5,7 @@ var Hawk = require('./hawk'); var Basic = require('./basic'); var Cookie = require('./cookie'); var Utils = require('../utils'); -var Err = require('../error'); +var Boom = require('boom'); var Log = require('hapi-log'); @@ -30,6 +30,7 @@ exports = module.exports = internals.Auth = function (server, options) { // Load strategies this.strategies = {}; + this.extensions = []; for (var name in settings) { if (settings.hasOwnProperty(name)) { var strategy = settings[name]; @@ -46,6 +47,12 @@ exports = module.exports = internals.Auth = function (server, options) { case 'cookie': this.strategies[name] = new Cookie(this.server, strategy); break; default: this.strategies[name] = strategy.implementation; break; } + + if (this.strategies[name].extend && + typeof this.strategies[name].extend === 'function') { + + this.extensions.push(this.strategies[name]); + } } } @@ -55,6 +62,14 @@ exports = module.exports = internals.Auth = function (server, options) { internals.Auth.authenticate = function (request, next) { + // Extend requests with loaded strategies + + if (request.server.auth) { + for (var i = 0, il = request.server.auth.extensions.length; i < il; ++i) { + request.server.auth.extensions[i].extend(request); + } + } + // Modes: required, optional, try, none var config = request.route.auth; @@ -97,7 +112,7 @@ internals.Auth.prototype.authenticate = function (request, next) { return next(); } - return next(Err.unauthorized('Missing authentication', authErrors)); + return next(Boom.unauthorized('Missing authentication', authErrors)); } var strategy = self.strategies[config.strategies[strategyPos++]]; // Increments counter after fetching current strategy @@ -109,7 +124,7 @@ internals.Auth.prototype.authenticate = function (request, next) { // Unauthenticated if (!err && !session) { - return next(Err.internal('Authentication response missing both error and session')); + return next(Boom.internal('Authentication response missing both error and session')); } if (err) { @@ -117,16 +132,13 @@ internals.Auth.prototype.authenticate = function (request, next) { request.log(['auth', 'unauthenticated'], err); } - if (err instanceof Error === false) { // Not an actual error (e.g. redirect, custom response) - return next(err); - } - - if (!err.isMissing || - err.code !== 401) { // An actual error (not just missing authentication) + if (err instanceof Error === false || // Not an actual error (e.g. redirect, custom response) + !err.isMissing || // Missing authentication (did not fail) + err.response.code !== 401) { // An actual error (not just missing authentication) if (config.mode === 'try') { request.session = session; - request.log(['auth', 'unauthenticated', 'try']); + request.log(['auth', 'unauthenticated', 'try'], err); return next(); } @@ -135,9 +147,8 @@ internals.Auth.prototype.authenticate = function (request, next) { // Try next strategy - var response = err.toResponse(); - if (response.headers['WWW-Authenticate']) { - authErrors.push(response.headers['WWW-Authenticate']); + if (err.response.headers['WWW-Authenticate']) { + authErrors.push(err.response.headers['WWW-Authenticate']); } return authenticate(); @@ -153,7 +164,7 @@ internals.Auth.prototype.authenticate = function (request, next) { (!session.scope || session.scope.indexOf(config.scope) === -1)) { request.log(['auth', 'error', 'scope'], { got: session.scope, need: config.scope }); - return next(Err.forbidden('Insufficient scope (\'' + config.scope + '\' expected)')); + return next(Boom.forbidden('Insufficient scope (\'' + config.scope + '\' expected)')); } // Check TOS @@ -163,14 +174,14 @@ internals.Auth.prototype.authenticate = function (request, next) { (!session.ext || !session.ext.tos || session.ext.tos < tos)) { request.log(['auth', 'error', 'tos'], { min: tos, received: session.ext && session.ext.tos }); - return next(Err.forbidden('Insufficient TOS accepted')); + return next(Boom.forbidden('Insufficient TOS accepted')); } // Check entity var entity = config.entity || 'any'; - // Entity: any + // Entity: 'any' if (entity === 'any') { request.log(['auth']); @@ -178,12 +189,12 @@ internals.Auth.prototype.authenticate = function (request, next) { return next(); } - // Entity: user + // Entity: 'user' if (entity === 'user') { if (!session.user) { request.log(['auth', 'error'], 'User session required'); - return next(Err.forbidden('Application session cannot be used on a user endpoint')); + return next(Boom.forbidden('Application session cannot be used on a user endpoint')); } request.log(['auth']); @@ -191,11 +202,11 @@ internals.Auth.prototype.authenticate = function (request, next) { return next(); } - // Entity: app + // Entity: 'app' if (session.user) { request.log(['auth', 'error'], 'App session required'); - return next(Err.forbidden('User session cannot be used on an application endpoint')); + return next(Boom.forbidden('User session cannot be used on an application endpoint')); } request.log(['auth']); diff --git a/lib/auth/oz.js b/lib/auth/oz.js index 1e187edfc..62dd90ece 100755 --- a/lib/auth/oz.js +++ b/lib/auth/oz.js @@ -2,7 +2,7 @@ var Oz = require('oz'); var Utils = require('../utils'); -var Err = require('../error'); +var Boom = require('boom'); // Declare internals diff --git a/lib/batch.js b/lib/batch.js index 50ba7ad6b..09a988f88 100755 --- a/lib/batch.js +++ b/lib/batch.js @@ -1,7 +1,7 @@ // Load modules var Async = require('async'); -var Err = require('./error'); +var Boom = require('boom'); // Declare internals @@ -42,7 +42,7 @@ exports.config = { }; if (!request.payload.requests) { - return request.reply(Err.badRequest('Request missing requests array')); + return request.reply(Boom.badRequest('Request missing requests array')); } for (var i = 0, il = request.payload.requests.length; i < il; ++i) { @@ -67,7 +67,7 @@ exports.config = { internals.process(request, requests, resultsData, request.reply); } else { - request.reply(Err.badRequest(errorMessage)); + request.reply(Boom.badRequest(errorMessage)); } } }; diff --git a/lib/defaults.js b/lib/defaults.js index 2e8ecb129..534b2fe61 100755 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -33,7 +33,8 @@ exports.server = { state: { cookies: { parse: true, // Parse content of req.headers.cookie - failAction: 'error' // Action on bad cookie - 'error': return 400, 'log': log and continue, 'ignore': continue + failAction: 'error', // Action on bad cookie - 'error': return 400, 'log': log and continue, 'ignore': continue + clearInvalid: false // Automatically instruct the client to remove the invalid cookie } }, diff --git a/lib/error.js b/lib/error.js deleted file mode 100755 index 1cf9df74e..000000000 --- a/lib/error.js +++ /dev/null @@ -1,11 +0,0 @@ -// Load modules - -var Boom = require('boom'); - - -// Declare internals - -var internals = {}; - - -exports = module.exports = Boom; diff --git a/lib/files.js b/lib/files.js index ac41fffa8..000ff7597 100755 --- a/lib/files.js +++ b/lib/files.js @@ -1,7 +1,7 @@ // Load modules var Response = require('./response'); -var Err = require('./error'); +var Boom = require('boom'); var Utils = require('./utils'); // Declare internals @@ -100,7 +100,7 @@ exports.directoryHandler = function (route, options) { if (request._paramsArray[0]) { if (request._paramsArray[0].indexOf('..') !== -1) { - return request.reply(Err.forbidden()); + return request.reply(Boom.forbidden()); } path += request._paramsArray[0]; diff --git a/lib/index.js b/lib/index.js index 4fad6b5fd..b243a4049 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2,7 +2,8 @@ var internals = { modules: { - error: require('./error'), + error: require('boom'), + boom: require('boom'), log: require('hapi-log').log, server: require('./server'), response: require('./response'), diff --git a/lib/notFound.js b/lib/notFound.js deleted file mode 100755 index 58fd00985..000000000 --- a/lib/notFound.js +++ /dev/null @@ -1,16 +0,0 @@ -// Load modules - -var Err = require('./error'); - -// Declare internals - -var internals = {}; - - -exports.handler = function (route) { - - return function (request) { - - return request.reply(Err.notFound('Not found')); - }; -}; \ No newline at end of file diff --git a/lib/payload.js b/lib/payload.js index b94cb4857..1c99ddf7a 100755 --- a/lib/payload.js +++ b/lib/payload.js @@ -3,7 +3,7 @@ var Zlib = require('zlib'); var Querystring = require('querystring'); var Formidable = require('formidable'); -var Err = require('./error'); +var Boom = require('boom'); // Declare internals @@ -43,7 +43,7 @@ exports.read = function (request, next) { if (contentLength && parseInt(contentLength, 10) > request.server.settings.payload.maxBytes) { - return next(Err.badRequest('Payload content length greater than maximum allowed: ' + request.server.settings.payload.maxBytes)); + return next(Boom.badRequest('Payload content length greater than maximum allowed: ' + request.server.settings.payload.maxBytes)); } var clientTimeout = request.server.settings.timeout.client; @@ -53,7 +53,7 @@ exports.read = function (request, next) { clientTimeout -= Date.now() - request._timestamp; clientTimeoutId = setTimeout(function () { - finish(Err.clientTimeout('Client is taking too long to send request')); + finish(Boom.clientTimeout('Client is taking too long to send request')); }, clientTimeout); } @@ -77,19 +77,19 @@ exports.read = function (request, next) { req.on('close', function () { - return finish(Err.internal('Request closed before finished reading')); + return finish(Boom.internal('Request closed before finished reading')); }); req.on('error', function (err) { - return finish(Err.internal('Request error before finished reading: ' + err)); + return finish(Boom.internal('Request error before finished reading: ' + err)); }); var payload = ''; req.on('data', function (chunk) { if (payload.length + chunk.length > request.server.settings.payload.maxBytes) { - return finish(Err.badRequest('Payload size greater than maximum allowed: ' + request.server.settings.payload.maxBytes)); + return finish(Boom.badRequest('Payload size greater than maximum allowed: ' + request.server.settings.payload.maxBytes)); } payload += chunk.toString(encoding); @@ -106,7 +106,7 @@ exports.read = function (request, next) { Zlib.unzip(new Buffer(payload, encoding), function (err, buffer) { if (err) { - return finish(Err.badRequest('Invalid gzip: ' + err)); + return finish(Boom.badRequest('Invalid gzip: ' + err)); } var unzipped = buffer.toString(); @@ -130,7 +130,7 @@ exports.read = function (request, next) { parserFunc(result, function (err, payload) { if (err) { - return finish(Err.badRequest('Invalid request payload format')); + return finish(Boom.badRequest('Invalid request payload format')); } request.payload = payload; @@ -167,7 +167,7 @@ internals.setParser = function (headers) { obj = JSON.parse(result); } catch (exp) { - return callback(Err.badRequest('Invalid request payload format')); + return callback(Boom.badRequest('Invalid request payload format')); } return callback(null, obj); @@ -212,7 +212,7 @@ internals.setParser = function (headers) { form.once('error', function () { form.removeAllListeners('end'); - return callback(Err.badRequest('Invalid request multipart payload format')); + return callback(Boom.badRequest('Invalid request multipart payload format')); }); form.once('end', function () { @@ -230,5 +230,5 @@ internals.setParser = function (headers) { // Other - return Err.badRequest('Unsupported content-type: ' + mime); + return Boom.badRequest('Unsupported content-type: ' + mime); }; \ No newline at end of file diff --git a/lib/proxy.js b/lib/proxy.js index 958fb93b6..82122d9f9 100755 --- a/lib/proxy.js +++ b/lib/proxy.js @@ -2,7 +2,7 @@ var Request = require('request'); var Utils = require('./utils'); -var Err = require('./error'); +var Boom = require('boom'); // Declare internals @@ -79,7 +79,7 @@ internals.Proxy.prototype.handler = function () { // Request handles all redirect responses (3xx) and will return an err if redirection fails if (err) { - return request.reply(Err.internal('Proxy error', err)); + return request.reply(Boom.internal('Proxy error', err)); } return self.settings.postResponse(request, self.settings, response, payload); @@ -134,7 +134,7 @@ internals.postResponse = function (request, settings, response, payload) { var statusCode = response.statusCode; if (statusCode >= 400) { - return request.reply(Err.passThrough(statusCode, payload, contentType)); + return request.reply(Boom.passThrough(statusCode, payload, contentType)); } var response = request.reply.payload(payload); diff --git a/lib/request.js b/lib/request.js index b171b4c50..50fb78f3c 100755 --- a/lib/request.js +++ b/lib/request.js @@ -7,7 +7,7 @@ var Stream = require('stream'); var Url = require('url'); var Async = require('async'); var Utils = require('./utils'); -var Err = require('./error'); +var Boom = require('boom'); var Payload = require('./payload'); var State = require('./state'); var Auth = require('./auth'); @@ -30,6 +30,8 @@ exports = module.exports = internals.Request = function (server, req, res, optio Utils.assert(req, 'req must be provided'); Utils.assert(res, 'res must be provided'); + options = options || {}; + // Pause request req.pause(); // Must be done before switching event execution context @@ -45,19 +47,29 @@ exports = module.exports = internals.Request = function (server, req, res, optio this._setMethod(req.method); // Sets: method this.id = now + '-' + process.pid + '-' + Math.floor(Math.random() * 0x10000); + this.app = {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins this.plugins = {}; // Place for plugins to store state without conflicts with hapi, should be namespaced using plugin name this.route = {}; this.response = null; this.isReplied = false; + this.session = null; // Does not mean authenticated { id, [app], [scope], [user], [ext.tos] } + this.isAuthenticated = false; + + // Apply options + + if (options.session) { + this.session = options.session; + } + // Defined elsewhere: // query // params // rawBody // payload - // session: { id, [app], [scope], [user], [ext.tos] } + // state // setUrl() @@ -103,14 +115,6 @@ exports = module.exports = internals.Request = function (server, req, res, optio this._debug = this.query[this.server.settings.debug.queryKey]; } - // Apply options - - if (options && - options.session) { - - this.session = options.session; - } - // Log request var about = { @@ -255,8 +259,8 @@ internals.Request.prototype._onRequestExt = function (callback) { // Send error response - self._route = self.server._router.notFound; // Only settings are used, not the handler - self.route = self.server._router.notFound.config; + self._route = self.server._router.notfound; // Only settings are used, not the handler + self.route = self.server._router.notfound.config; self._reply(err, function () { return callback(err); @@ -277,7 +281,7 @@ internals.Request.prototype._execute = function (route) { serverTimeout -= (Date.now() - self._timestamp); // Calculate the timeout from when the request was constructed var timeoutReply = function () { - self._reply(Err.serverTimeout()); + self._reply(Boom.serverTimeout()); }; if (serverTimeout <= 0) { @@ -336,7 +340,8 @@ internals.Request.prototype._reply = function (err, callback) { if (err) { if (self.response && - self.response instanceof Error === false) { + !self.response.isBoom && + !self.response.varieties.error) { // Got error after valid result was already set @@ -351,7 +356,7 @@ internals.Request.prototype._reply = function (err, callback) { var sendResponse = function () { - if (!self.response) { // Can only happen when request.reply.close() is called + if (!self.response) { // Can only happen when request.reply.close() is called self.raw.res.end(); // End the response in case it wasn't already closed return finalize(); } @@ -591,7 +596,9 @@ internals.handler = function (request, next) { // Check for Error result - if (response instanceof Error) { + if (response && + (response.isBoom || response.varieties.error)) { + request.log(['handler', 'result', 'error'], { msec: timer.elapsed() }); return callback(response); } diff --git a/lib/response/base.js b/lib/response/base.js index cba803e9a..5002cf23b 100755 --- a/lib/response/base.js +++ b/lib/response/base.js @@ -18,6 +18,7 @@ exports = module.exports = internals.Base = function () { this._flags = {}; // Cached this._states = {}; // Not cached + this._wasPrepared = false; return this; }; @@ -66,6 +67,13 @@ internals.Base.prototype.unstate = function (name) { }; +internals.Base.prototype._prepare = function (request, callback) { + + this._wasPrepared = true; + return callback(this); +}; + + // Required interface /* diff --git a/lib/response/directory.js b/lib/response/directory.js index d553c9208..17064a704 100755 --- a/lib/response/directory.js +++ b/lib/response/directory.js @@ -4,7 +4,7 @@ var Fs = require('fs'); var Path = require('path'); var NodeUtil = require('util'); var Cacheable = require('./cacheable'); -var Err = require('../error'); +var Boom = require('boom'); var File = require('./file'); var Utils = require('../utils'); @@ -42,6 +42,8 @@ internals.Directory.prototype._prepare = function (request, callback) { var self = this; + this._wasPrepared = true; + var finalize = function (response) { if (response instanceof Error) { @@ -52,7 +54,7 @@ internals.Directory.prototype._prepare = function (request, callback) { }; if (this._hideFile(this._path)) { // Don't serve hidden files when showHidden is disabled - return finalize(Err.notFound()); + return finalize(Boom.notFound()); } // Lookup file @@ -67,8 +69,9 @@ internals.Directory.prototype._prepare = function (request, callback) { // Not found - if (response.code !== 403) { - return finalize(response); + var error = response; + if (error.response.code !== 403) { + return finalize(error); } // Directory @@ -76,7 +79,7 @@ internals.Directory.prototype._prepare = function (request, callback) { if (!self._index && !self._listing) { - return finalize(Err.forbidden()); + return finalize(Boom.forbidden()); } if (!self._index) { @@ -94,14 +97,15 @@ internals.Directory.prototype._prepare = function (request, callback) { // Directory - if (indexResponse.code !== 404) { - return finalize(Err.internal('index.html is a directory')); + var error = indexResponse; + if (error.response.code !== 404) { + return finalize(Boom.internal('index.html is a directory')); } // Not found if (!self._listing) { - return finalize(Err.forbidden()); + return finalize(Boom.forbidden()); } return self._generateListing(finalize); @@ -117,7 +121,7 @@ internals.Directory.prototype._generateListing = function (callback) { Fs.readdir(this._path, function (err, files) { if (err) { - return callback(Err.internal('Error accessing directory')); + return callback(Boom.internal('Error accessing directory')); } var separator = ''; diff --git a/lib/response/error.js b/lib/response/error.js index c886ab1f7..54c88e9dd 100755 --- a/lib/response/error.js +++ b/lib/response/error.js @@ -14,7 +14,11 @@ var internals = {}; exports = module.exports = internals.Error = function (options) { - // { code, payload, type, headers } + // { code, payload, type, headers } or Boom + + if (options.isBoom) { + options = options.response; + } Obj.call(this, options.payload); this.variety = 'error'; diff --git a/lib/response/file.js b/lib/response/file.js index b2e34cf3b..c0c96f8a5 100755 --- a/lib/response/file.js +++ b/lib/response/file.js @@ -4,17 +4,19 @@ var Fs = require('fs'); var Path = require('path'); var NodeUtil = require('util'); var Mime = require('mime'); -var Err = require('../error'); +var Boom = require('boom'); var Utils = require('../utils'); var Stream = require('./stream'); -var LRU = require('lru-cache'); +var LruCache = require('lru-cache'); var Crypto = require('crypto'); // Declare internals -var internals = {}; -internals.fileEtags = LRU(); +var internals = { + fileEtags: LruCache() // Files etags cache +}; + // File response (Base -> Generic -> Stream -> File) @@ -37,38 +39,49 @@ NodeUtil.inherits(internals.File, Stream); internals.File.prototype._prepare = function (request, callback) { var self = this; + + this._wasPrepared = true; + Fs.stat(self._filePath, function (err, stat) { if (err) { - return callback(Err.notFound()); + return callback(Boom.notFound()); } if (stat.isDirectory()) { - return callback(Err.forbidden()); + return callback(Boom.forbidden()); } var fileName = Path.basename(self._filePath); var stream = Fs.createReadStream(self._filePath); + Stream.call(self, stream); + self._headers['Content-Type'] = Mime.lookup(self._filePath) || 'application/octet-stream'; self._headers['Content-Length'] = stat.size; - + // Use stat info for an LRU cache key. + var cachekey = [self._filePath, stat.ino, stat.size, Date.parse(stat.mtime)].join('-'); - - // The etag must hash the file contents in order to be consistent between nodes. + + // The etag must hash the file contents in order to be consistent across distributed deployments + if (internals.fileEtags.has(cachekey)) { self._headers.etag = JSON.stringify(internals.fileEtags.get(cachekey)); - } else { + } + else { var hash = Crypto.createHash('md5'); stream.on('data', function (chunk) { + hash.update(chunk); - }) + }); + stream.on('end', function () { - internals.fileEtags.set(cachekey, hash.digest("hex")); - }) + + internals.fileEtags.set(cachekey, hash.digest('hex')); + }); } - + self._headers['Content-Disposition'] = 'inline; filename=' + encodeURIComponent(fileName); return Stream.prototype._prepare.call(self, request, callback); diff --git a/lib/response/generic.js b/lib/response/generic.js index 5dd598518..5c54a8e62 100755 --- a/lib/response/generic.js +++ b/lib/response/generic.js @@ -5,7 +5,7 @@ var Zlib = require('zlib'); var Base = require('./base'); var Headers = require('./headers'); var Utils = require('../utils'); -var Err = require('../error'); +var Boom = require('boom'); // Declare internals @@ -37,6 +37,8 @@ internals.Generic.prototype._prepare = function (request, callback) { var self = this; + this._wasPrepared = true; + Headers.state(this, request, function (err) { if (err) { diff --git a/lib/response/headers.js b/lib/response/headers.js index bc340232e..a5e528770 100755 --- a/lib/response/headers.js +++ b/lib/response/headers.js @@ -88,4 +88,3 @@ exports.state = function (response, request, callback) { return callback(); }); }; - diff --git a/lib/response/index.js b/lib/response/index.js index c87e3a659..e883e991a 100755 --- a/lib/response/index.js +++ b/lib/response/index.js @@ -1,14 +1,16 @@ // Load modules var Stream = require('stream'); +var Boom = require('boom'); var Shot = require('shot'); var Utils = require('../utils'); -var Err = require('../error'); // Declare internals -var internals = {}; +var internals = { + maxNestedPreparations: 5 +}; /* @@ -67,11 +69,13 @@ exports.generate = function (result, onSend) { response = new internals.Text(result); } else if (typeof result === 'object') { - if (result.variety) { + if (result.variety || + result.isBoom) { + response = result; } else if (result instanceof Error) { - response = new Err(result); + response = new Boom(result); } else if (result instanceof Stream) { response = new internals.Stream(result); @@ -82,7 +86,7 @@ exports.generate = function (result, onSend) { response = new internals.Obj(result); } - Utils.assert(response && (response.variety || response instanceof Error), 'Response must be an instance of Error or Generic'); // Safety + Utils.assert(response && (response.variety || response.isBoom), 'Invalid response object'); // Safety if (onSend) { response.send = function () { @@ -99,54 +103,94 @@ exports.generate = function (result, onSend) { exports._respond = function (item, request, callback) { var errorPrepared = false; + var prepareCount = 0; var prepare = function (response) { if (!response || - (!response.variety && response instanceof Err === false)) { + (!response.variety && !response.isBoom)) { - response = Err.internal('Unexpected response item', response); + response = Boom.internal('Unexpected response item', response); } - if (response._prepare && - typeof response._prepare === 'function') { + if (response.isBoom) { + response = new internals.Error(response); + } - return response._prepare(request, send); + if (!response._prepare) { + return etag(response); } - return send(response); - }; + response._prepare(request, function (result) { - var send = function (response) { + if (!result._wasPrepared) { + ++prepareCount; + if (prepareCount > internals.maxNestedPreparations) { // Prevent prepare loops + result = new internals.Error(Boom.internal('Response prepare count exceeded maximum allowed', item)); + return send(result); + } - // Error object + if (result.isBoom) { + if (errorPrepared) { + result = new internals.Error(result); + return send(result); + } - if (response instanceof Err) { - request.log(['http', 'response'], response); - response = new internals.Error(response.toResponse()); + errorPrepared = true; // Prevent error loops + } - if (!errorPrepared) { // Prevents a loop if _prepare returns an error - errorPrepared = true; - return response._prepare(request, send); + return prepare(result); } + + return etag(result); + }); + }; + + var etag = function (response) { + + if (request.method !== 'get' && + request.method !== 'head') { + + return send(response); } - if (request.method === 'get' || request.method === 'head') { + // Process ETag - // Process ETag and If-Modified-Since headers + var etag = response._headers && response._headers.etag; + if (etag && + request.raw.req.headers['if-none-match'] === etag) { - var ifModifiedSince = request.raw.req.headers['if-modified-since'] ? Date.parse(request.raw.req.headers['if-modified-since']) : null; - var lastModified = response._headers && response._headers['Last-Modified'] ? Date.parse(response._headers['Last-Modified']) : null; - var etag = response._headers ? response._headers.etag : null; + var unchanged = new internals.Empty(); + unchanged._code = 304; + return prepare(unchanged); + } + + // Process If-Modified-Since headers + + var ifModifiedSinceHeader = request.raw.req.headers['if-modified-since']; + var lastModifiedHeader = response._headers && response._headers['Last-Modified']; - if ((etag && request.raw.req.headers['if-none-match'] === etag) || - (ifModifiedSince && lastModified && ifModifiedSince >= lastModified)) { + if (ifModifiedSinceHeader && + lastModifiedHeader) { - response = new internals.Empty(); - response._code = 304; + var ifModifiedSince = Date.parse(ifModifiedSinceHeader); + var lastModified = Date.parse(lastModifiedHeader); + + if (ifModifiedSince && + lastModified && + ifModifiedSince >= lastModified) { + + var unchanged = new intenals.Empty(); + unchanged._code = 304; + return prepare(unchanged); } } + return send(response); + }; + + var send = function (response) { + // Injection if (response._payload !== undefined) { // Value can be falsey @@ -164,3 +208,5 @@ exports._respond = function (item, request, callback) { prepare(item); }; + + diff --git a/lib/response/obj.js b/lib/response/obj.js index b1f7383d1..e9d1dcc71 100755 --- a/lib/response/obj.js +++ b/lib/response/obj.js @@ -18,7 +18,7 @@ exports = module.exports = internals.Obj = function (object, type) { this.varieties.obj = true; this.raw = object; // Can change if reference is modified - this.update(type); // Convert immediately to snapshot content + this.update(type); // Convert immediately to snapshot content return this; }; diff --git a/lib/response/view.js b/lib/response/view.js index a1bf5b386..e17236466 100755 --- a/lib/response/view.js +++ b/lib/response/view.js @@ -32,6 +32,8 @@ NodeUtil.inherits(internals.View, Cacheable); internals.View.prototype._prepare = function (request, callback) { + this._wasPrepared = true; + this._payload = this.view.manager.render(this.view.template, this.view.context, this.view.options); if (this._payload instanceof Error) { return callback(this._payload); diff --git a/lib/route.js b/lib/route.js index b1307b59f..5ec58a2f7 100755 --- a/lib/route.js +++ b/lib/route.js @@ -1,9 +1,9 @@ // Load modules +var Boom = require('boom'); var Catbox = require('catbox'); var Auth = require('./auth'); var Files = require('./files'); -var NotFound = require('./notFound'); var Proxy = require('./proxy'); var Utils = require('./utils'); var Views = require('./views'); @@ -32,7 +32,10 @@ exports = module.exports = internals.Route = function (options, server) { this.method = settings.method.toLowerCase(); this.path = settings.path; this.config = Utils.applyToDefaults(server.settings.router.routeDefaults, settings.config || {}); - this.config.plugins = this.config.plugins || {}; + this.config.method = this.method; // Expose method in config + + this.config.plugins = this.config.plugins || {}; // Route-specific plugins config, namespaced using plugin name + this.config.app = this.config.app || {}; // Route-specific application config Utils.assert(this.method !== 'head', 'Cannot add a HEAD route'); Utils.assert(!!settings.handler ^ !!this.config.handler, 'Handler must appear once and only once'); // XOR @@ -166,7 +169,7 @@ exports = module.exports = internals.Route = function (options, server) { } } else if (this.config.handler === 'notFound') { - this.config.handler = NotFound.handler(this); + this.config.handler = internals.notFound(); } return this; @@ -387,3 +390,12 @@ exports.sort = function (a, b) { return (al > bl ? bFirst : aFirst); }; + +internals.notFound = function () { + + return function (request) { + + return request.reply(Boom.notFound()); + }; +}; + diff --git a/lib/server.js b/lib/server.js index 4c9877b85..0f2e3ec46 100755 --- a/lib/server.js +++ b/lib/server.js @@ -11,7 +11,7 @@ var Auth = require('./auth'); var Batch = require('./batch'); var Catbox = require('catbox'); var Defaults = require('./defaults'); -var Err = require('./error'); +var Boom = require('boom'); var Request = require('./request'); var Route = require('./route'); var Views = require('./views'); @@ -108,8 +108,8 @@ module.exports = internals.Server = function (/* host, port, options */) { table: {} // Array per HTTP method, including * for catch-all }; - this._router.notFound = new Route({ - method: 'notFound', + this._router.notfound = new Route({ + method: 'notfound', path: '/{p*}', config: { auth: { mode: 'none' }, // In case defaults are set otherwise @@ -122,6 +122,7 @@ module.exports = internals.Server = function (/* host, port, options */) { this._pack = null; this.plugins = {}; // Registered plugin APIs by plugin name this.plugin.list = {}; // Loaded plugins by plugin name + this.app = {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins // Initialize Views @@ -270,7 +271,7 @@ internals.Server.prototype._dispatch = function (options) { // Not found - return request._execute(self._router.notFound); + return request._execute(self._router.notfound); }); }; }; diff --git a/lib/state.js b/lib/state.js index b0d4a988a..6e17634e9 100755 --- a/lib/state.js +++ b/lib/state.js @@ -4,7 +4,7 @@ var Querystring = require('querystring'); var Iron = require('iron'); var Async = require('async'); var Cryptiles = require('cryptiles'); -var Err = require('./error'); +var Boom = require('boom'); var Utils = require('./utils'); @@ -71,10 +71,14 @@ exports.parseCookies = function (request, next) { var shouldStop = function (error, name) { + if (request.server.settings.state.cookies.clearInvalid) { + request.clearState(name); + } + // failAction: 'error', 'log', 'ignore' if (request.server.settings.state.cookies.failAction === 'error') { - next(Err.badRequest('Bad cookie ' + (name ? 'value: ' + name : 'header'))); + next(Boom.badRequest('Bad cookie ' + (name ? 'value: ' + name : 'header'))); return true; } @@ -169,6 +173,8 @@ exports.parseCookies = function (request, next) { }, function (err) { + // All cookies parsed + return next(); }); }; @@ -185,19 +191,19 @@ exports.parseCookies = function (request, next) { var pos = value.lastIndexOf('.'); if (pos === -1) { - return callback(Err.internal('Missing signature separator')); + return callback(Boom.internal('Missing signature separator')); } var unsigned = value.slice(0, pos); var sig = value.slice(pos + 1); if (!sig) { - return callback(Err.internal('Missing signature')); + return callback(Boom.internal('Missing signature')); } sigParts = sig.split('*'); if (sigParts.length !== 2) { - return callback(Err.internal('Bad signature format')); + return callback(Boom.internal('Bad signature format')); } var hmacSalt = sigParts[0]; @@ -212,7 +218,7 @@ exports.parseCookies = function (request, next) { } if (!Cryptiles.fixedTimeComparison(mac.digest, hmac)) { - return callback(Err.internal('Bad hmac value')); + return callback(Boom.internal('Bad hmac value')); } return callback(null, unsigned); @@ -303,7 +309,7 @@ exports.generateSetCookieHeader = function (cookies, definitions, callback) { // Validate name if (!cookie.name.match(internals.nameRegx)) { - return callback(Err.internal('Invalid cookie name: ' + cookie.name)); + return callback(Boom.internal('Invalid cookie name: ' + cookie.name)); } // Encode value @@ -311,7 +317,7 @@ exports.generateSetCookieHeader = function (cookies, definitions, callback) { encode(cookie.value, options, function (err, value) { if (err) { - return callback(Err.internal('Failed to encode cookie (' + cookie.name + ') value' + (err.message ? ': ' + err.message : ''))); + return callback(Boom.internal('Failed to encode cookie (' + cookie.name + ') value' + (err.message ? ': ' + err.message : ''))); } // Validate value @@ -319,7 +325,7 @@ exports.generateSetCookieHeader = function (cookies, definitions, callback) { if (value && (typeof value !== 'string' || !value.match(internals.valueRegx))) { - return callback(Err.internal('Invalid cookie value: ' + cookie.value)); + return callback(Boom.internal('Invalid cookie value: ' + cookie.value)); } // Sign cookie @@ -327,7 +333,7 @@ exports.generateSetCookieHeader = function (cookies, definitions, callback) { sign(cookie.name, value, options.sign, function (err, signed) { if (err) { - return callback(Err.internal('Failed to sign cookie (' + cookie.name + ') value' + (err.message ? ': ' + err.message : ''))); + return callback(Boom.internal('Failed to sign cookie (' + cookie.name + ') value' + (err.message ? ': ' + err.message : ''))); } // Construct cookie @@ -352,11 +358,11 @@ exports.generateSetCookieHeader = function (cookies, definitions, callback) { if (options.domain) { var domain = options.domain.toLowerCase(); if (!domain.match(internals.domainLabelLenRegx)) { - return callback(Err.internal('Cookie domain too long: ' + options.domain)); + return callback(Boom.internal('Cookie domain too long: ' + options.domain)); } if (!domain.match(internals.domainRegx)) { - return callback(Err.internal('Invalid cookie domain: ' + options.domain)); + return callback(Boom.internal('Invalid cookie domain: ' + options.domain)); } segment += '; Domain=' + domain; @@ -364,7 +370,7 @@ exports.generateSetCookieHeader = function (cookies, definitions, callback) { if (options.path) { if (!options.path.match(internals.pathRegx)) { - return callback(Err.internal('Invalid cookie path: ' + options.path)); + return callback(Boom.internal('Invalid cookie path: ' + options.path)); } segment += '; Path=' + options.path; diff --git a/lib/validation.js b/lib/validation.js index 8be8b3df9..5e0b24709 100755 --- a/lib/validation.js +++ b/lib/validation.js @@ -1,7 +1,7 @@ // Load modules var Joi = require('joi'); -var Err = require('./error'); +var Boom = require('boom'); var Response = require('./response'); @@ -27,7 +27,7 @@ exports.query = function (request, next) { Joi.validate(request.query, request.route.validate.query, function (err) { - next(err ? Err.badRequest(err.message) : null); + next(err ? Boom.badRequest(err.message) : null); }); }; @@ -49,7 +49,7 @@ exports.payload = function (request, next) { Joi.validate(request.payload, request.route.validate.schema, function (err) { - next(err ? Err.badRequest(err.message) : null); + next(err ? Boom.badRequest(err.message) : null); }); }; @@ -71,7 +71,7 @@ exports.path = function (request, next) { Joi.validate(request.params, request.route.validate.path, function (err) { - next(err ? Err.badRequest(err.message) : null); + next(err ? Boom.badRequest(err.message) : null); }); }; @@ -99,12 +99,14 @@ exports.response = function (request, next) { } } - if (request.response instanceof Error) { + if (request.response.isBoom || + request.response.varieties.error) { + return next(); } if (!request.response.varieties.obj) { - return next(Err.internal('Cannot validate non-object response')); + return next(Boom.internal('Cannot validate non-object response')); } Joi.validate(request.response.raw, request.route.response.schema, function (err) { @@ -121,7 +123,7 @@ exports.response = function (request, next) { return next(); } - next(Err.internal(err.message)); + next(Boom.internal(err.message)); }); }; diff --git a/lib/views.js b/lib/views.js index 74903f2e2..d7c25226d 100755 --- a/lib/views.js +++ b/lib/views.js @@ -4,7 +4,7 @@ var Fs = require('fs'); var Path = require('path'); var Defaults = require('./defaults'); var Utils = require('./utils'); -var Err = require('./error'); +var Boom = require('boom'); // Additional engine modules required in constructor @@ -177,7 +177,7 @@ internals.Manager.prototype.render = function (filename, context, options) { return this.execute(engine, template, context, options)(context, options); } catch (err) { - return Err.internal(err.message, err); + return Boom.internal(err.message, err); } } @@ -186,7 +186,7 @@ internals.Manager.prototype.render = function (filename, context, options) { if (context && context.hasOwnProperty(this.settings.layoutKeyword)) { - return Err.internal('settings.layoutKeyword conflict', { context: context, keyword: this.settings.layoutKeyword }); + return Boom.internal('settings.layoutKeyword conflict', { context: context, keyword: this.settings.layoutKeyword }); } var layout = this._get('layout', options); @@ -201,7 +201,7 @@ internals.Manager.prototype.render = function (filename, context, options) { return this.execute(engine, layout, layoutContext, options)(layoutContext); } catch (err) { - return Err.internal(err.message, err); + return Boom.internal(err.message, err); } }; @@ -257,13 +257,13 @@ internals.Manager.prototype._get = function (filename, options) { if (!options.allowAbsolutePaths && isAbsolutePath) { - return Err.internal('Absolute paths are not allowed in views'); + return Boom.internal('Absolute paths are not allowed in views'); } if (!options.allowInsecureAccess && isInsecurePath) { - return Err.internal('View paths cannot lookup templates outside root path (path includes one or more \'../\')'); + return Boom.internal('View paths cannot lookup templates outside root path (path includes one or more \'../\')'); } // Resolve Path and extension @@ -295,7 +295,7 @@ internals.Manager.prototype._get = function (filename, options) { var source = Fs.readFileSync(fullPath).toString(this.settings.encoding); } catch (e) { - return Err.internal('View file not found: ' + fullPath); + return Boom.internal('View file not found: ' + fullPath); } var engine = this.engines.get(fullPath); diff --git a/package.json b/package.json index fcd76c658..4f2def4bb 100755 --- a/package.json +++ b/package.json @@ -22,22 +22,22 @@ }, "dependencies": { "hoek": "0.4.x", - "boom": "0.2.x", + "boom": "0.3.x", "joi": "0.0.x", "hapi-helmet": "0.0.x", "hapi-log": "0.1.x", - "hawk": "0.6.x", + "hawk": "0.7.x", "shot": "0.0.x", - "oz": "0.0.x", + "oz": "0.1.x", "async": "0.1.x", "request": "2.11.x", "formidable": "1.0.x", "mime": "1.2.x", "catbox": "0.1.x", - "cryptiles": "0.0.x", - "iron": "0.1.x", + "cryptiles": "0.1.x", + "iron": "0.2.x", "semver": "1.1.0", - "lru-cache": "~2.2.2" + "lru-cache": "2.2.x" }, "devDependencies": { "mocha": "1.x.x", diff --git a/test/integration/auth.js b/test/integration/auth.js index 40f7ec5f1..702a8e5b0 100755 --- a/test/integration/auth.js +++ b/test/integration/auth.js @@ -937,7 +937,7 @@ describe('Auth', function () { expect(res.statusCode).to.equal(200); expect(res.result).to.equal('logged-out'); - expect(res.headers['Set-Cookie'][0]).to.equal('special=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure'); + expect(res.headers['Set-Cookie'][0]).to.equal('special=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; Path=/'); done(); }); }); @@ -955,7 +955,7 @@ describe('Auth', function () { server.inject({ method: 'GET', url: '/resource', headers: { cookie: 'special=' + cookie[1] } }, function (res) { - expect(res.headers['Set-Cookie'][0]).to.equal('special=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure'); + expect(res.headers['Set-Cookie'][0]).to.equal('special=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; Path=/'); expect(res.statusCode).to.equal(401); done(); }); @@ -1027,6 +1027,37 @@ describe('Auth', function () { done(); }); }); + + it('does not redirect on try', function (done) { + + var config = { + scheme: 'cookie', + password: 'password', + ttl: 60 * 1000, + redirectTo: 'http://example.com/login', + appendNext: true, + validateFunc: function (session, callback) { + + return callback(); + } + }; + + var server = new Hapi.Server({ auth: config }); + + server.route({ + method: 'GET', path: '/', config: { auth: { mode: 'try' } }, handler: function () { + + return this.reply('try'); + } + }); + + server.inject({ method: 'GET', url: '/' }, function (res) { + + expect(res.result).to.equal('try'); + expect(res.statusCode).to.equal(200); + done(); + }); + }); }); }); }); \ No newline at end of file diff --git a/test/integration/notFound.js b/test/integration/notFound.js index 48df494cd..254788cc7 100755 --- a/test/integration/notFound.js +++ b/test/integration/notFound.js @@ -61,7 +61,7 @@ describe('NotFound', function () { server.route({ method: 'GET', path: '/exists/{p*}', handler: function (request) { request.reply('OK'); } }); server.route({ method: '*', path: '/{p*}', handler: function (request) { - request.reply(Hapi.Error.notFound('These these aren\'t the pages you\'re looking for.')); + request.reply(Hapi.error.notFound('These these aren\'t the pages you\'re looking for.')); }}); it('returns custom response when requesting a route that doesn\'t exist', function (done) { diff --git a/test/integration/proxy.js b/test/integration/proxy.js index f5bed1ffb..8d1d5bac1 100755 --- a/test/integration/proxy.js +++ b/test/integration/proxy.js @@ -115,12 +115,12 @@ describe('Proxy', function () { function unauthorized (request) { - request.reply(Hapi.Error.unauthorized('Not authorized')); + request.reply(Hapi.error.unauthorized('Not authorized')); } function postResponseWithError (request) { - request.reply(Hapi.Error.forbidden('Forbidden')); + request.reply(Hapi.error.forbidden('Forbidden')); } function postResponse (request, settings, response, payload) { diff --git a/test/integration/response.js b/test/integration/response.js index cb38eee4d..32cbe15a8 100755 --- a/test/integration/response.js +++ b/test/integration/response.js @@ -20,33 +20,6 @@ var expect = Chai.expect; describe('Response', function () { - describe('External', function () { - - it('returns a reply', function (done) { - - var handler = function () { - - this.reply.close(); - }; - - var server = new Hapi.Server({ cache: { engine: 'memory' } }); - server.route({ method: 'GET', path: '/throw', config: { handler: handler, cache: { mode: 'server', expiresIn: 9999 } } }); - server.route({ method: 'GET', path: '/null', config: { handler: handler } }); - - server.inject({ method: 'GET', url: '/null' }, function (res) { - - expect(res.readPayload()).to.equal('0\r\n\r\n'); - - expect(function () { - - server.inject({ method: 'GET', url: '/throw' }, function (res) { }); - }).to.throw(); - - done(); - }); - }); - }); - describe('Text', function () { it('returns a text reply', function (done) { @@ -1613,6 +1586,118 @@ describe('Response', function () { }); }); + describe('External', function () { + + it('returns a reply', function (done) { + + var handler = function () { + + this.raw.res.end(); + this.reply.close(); + }; + + var server = new Hapi.Server({ cache: { engine: 'memory' } }); + server.route({ method: 'GET', path: '/throw', config: { handler: handler, cache: { mode: 'server', expiresIn: 9999 } } }); + server.route({ method: 'GET', path: '/null', config: { handler: handler } }); + + server.inject({ method: 'GET', url: '/null' }, function (res) { + + expect(res.readPayload()).to.equal('0\r\n\r\n'); + + expect(function () { + + server.inject({ method: 'GET', url: '/throw' }, function (res) { }); + }).to.throw(); + + done(); + }); + }); + }); + + describe('Extension', function () { + + it('returns a reply using custom response without _prepare', function (done) { + + var handler = function () { + + var custom = { + variety: 'x-custom', + varieties: { 'x-custom': true }, + _transmit: function (request, callback) { + + request.raw.res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': 11 }); + request.raw.res.end('Hello World'); + } + }; + + this.reply(custom); + }; + + var server = new Hapi.Server(); + server.route({ method: 'GET', path: '/', config: { handler: handler } }); + + server.inject({ method: 'GET', url: '/' }, function (res) { + + expect(res.readPayload()).to.equal('Hello World'); + done(); + }); + }); + + it('returns an internal error on error response loop', function (done) { + + var handler = function () { + + var custom = { + variety: 'x-custom', + varieties: { 'x-custom': true }, + _prepare: function (request, callback) { + + callback(Hapi.error.badRequest()); + }, + _transmit: function () { } + }; + + this.setState('bad', {}); + this.reply(custom); + }; + + var server = new Hapi.Server(); + server.route({ method: 'GET', path: '/', config: { handler: handler } }); + + server.inject({ method: 'GET', url: '/' }, function (res) { + + expect(res.result.code).to.equal(500); + done(); + }); + }); + + it('returns an error on infinite _prepare loop', function (done) { + + var handler = function () { + + var custom = { + variety: 'x-custom', + varieties: { 'x-custom': true }, + _prepare: function (request, callback) { + + callback(custom); + } + }; + + this.reply(custom); + }; + + var server = new Hapi.Server(); + server.route({ method: 'GET', path: '/', config: { handler: handler } }); + + server.inject({ method: 'GET', url: '/' }, function (res) { + + expect(res.result.code).to.equal(500); + done(); + }); + }); + }); + describe('#_respond', function () { it('returns an error reply on invalid Response._respond', function (done) { diff --git a/test/unit/auth/index.js b/test/unit/auth/index.js index 80cdc1fb4..180a2f086 100755 --- a/test/unit/auth/index.js +++ b/test/unit/auth/index.js @@ -137,7 +137,7 @@ describe('Auth', function () { req: { headers: { host: 'localhost', - authorization: 'basic d2FsbWFydDp3YWxtYXJ0' + authorization: 'basic ' + (new Buffer('steve:password').toString('base64')) }, url: 'http://localhost/test' } @@ -155,7 +155,7 @@ describe('Auth', function () { scheme: 'basic', loadUserFunc: function (username, callback) { - return callback(null, { id: 'walmart', password: 'walmart' }); + return callback(null, { id: 'steve', password: 'password' }); } } }; @@ -188,7 +188,7 @@ describe('Auth', function () { scheme: 'basic', loadUserFunc: function (username, callback) { - return callback(null, { id: 'walmart', password: 'walmart' }); + return callback(null, { id: 'steve', password: 'password' }); } } } diff --git a/test/unit/response/directory.js b/test/unit/response/directory.js index b0ce0b448..c37e74817 100755 --- a/test/unit/response/directory.js +++ b/test/unit/response/directory.js @@ -25,7 +25,7 @@ describe('Response', function () { var dir = new Hapi.response.Directory('no_such_path', {}); dir._generateListing(function (response) { - expect(response.code).to.equal(500); + expect(response.response.code).to.equal(500); done(); }); }); diff --git a/test/unit/state.js b/test/unit/state.js index 5dd626fc6..170ce9413 100755 --- a/test/unit/state.js +++ b/test/unit/state.js @@ -107,6 +107,7 @@ describe('State', function () { it('fails parsing cookie header: ' + header, function (done) { var ignore = false; + var cleared = ''; var request = { raw: { @@ -124,6 +125,10 @@ describe('State', function () { }, log: function (tags, data) { ignore = true; + }, + clearState: function (name) { + + cleared = name; } }; @@ -136,6 +141,10 @@ describe('State', function () { expect(err).to.exist; } + if (request.server.settings.state.cookies.clearInvalid) { + expect(cleared).to.equal('sid'); + } + done(); }); }); @@ -162,6 +171,10 @@ describe('State', function () { fail('key=XeyJ0ZXN0aW5nIjoianNvbiJ9; key=XeyJ0ZXN0aW5dnIjoianNvbiJ9', setLog, { key: { encoding: 'base64json' } }); fail('sid=a=1&b=2&c=3%20x', setLog, { sid: { encoding: 'form', sign: { password: 'password' } } }); fail('sid=a=1&b=2&c=3%20x; sid=a=1&b=2&c=3%20x', setLog, { sid: { encoding: 'form', sign: { password: 'password' } } }); + + var clearInvalid = Hapi.utils.clone(Defaults.server.state); + clearInvalid.cookies.clearInvalid = true; + fail('sid=a=1&b=2&c=3%20x', clearInvalid, { sid: { encoding: 'form', sign: { password: 'password' } } }); }); }); diff --git a/test/unit/validation.js b/test/unit/validation.js index 5ed5b484d..3d197179f 100755 --- a/test/unit/validation.js +++ b/test/unit/validation.js @@ -75,7 +75,7 @@ describe('Validation', function () { it('should not raise an error when responding with valid param', function (done) { - var query = { username: 'walmart' }; + var query = { username: 'steve' }; var request = createRequestObject(query, route); request.response = Hapi.Response.generate({ username: 'test' }); @@ -89,10 +89,10 @@ describe('Validation', function () { it('an error response should skip response validation and not return an error', function (done) { - var query = { username: 'walmart' }; + var query = { username: 'steve' }; var request = createRequestObject(query, route); - request.response = Hapi.Response.generate(Hapi.Error.unauthorized('You are not authorized')); + request.response = Hapi.Response.generate(Hapi.error.unauthorized('You are not authorized')); Validation.response(request, function (err) { @@ -103,7 +103,7 @@ describe('Validation', function () { it('should raise an error when responding with invalid param', function (done) { - var query = { username: 'walmart' }; + var query = { username: 'steve' }; var request = createRequestObject(query, route); request.response = Hapi.Response.generate({ wrongParam: 'test' }); @@ -116,7 +116,7 @@ describe('Validation', function () { it('should not raise an error when responding with invalid param and sample is 0', function (done) { - var query = { username: 'walmart' }; + var query = { username: 'steve' }; var request = createRequestObject(query, route); request.route.response.sample = 0; request.response = Hapi.Response.generate({ wrongParam: 'test' }); @@ -130,7 +130,7 @@ describe('Validation', function () { it('should raise an error when responding with invalid param and sample is 100', function (done) { - var query = { username: 'walmart' }; + var query = { username: 'steve' }; var request = createRequestObject(query, route); request.route.response.sample = 100; request.response = Hapi.Response.generate({ wrongParam: 'test' }); @@ -144,7 +144,7 @@ describe('Validation', function () { internals.calculateFailAverage = function (size, sample) { - var query = { username: 'walmart' }; + var query = { username: 'steve' }; var request = createRequestObject(query, route); request.route.response.failAction = 'log'; request.route.response.sample = sample; @@ -186,7 +186,7 @@ describe('Validation', function () { it('should report an error when responding with invalid response param and failAction is report', function (done) { - var query = { username: 'walmart' }; + var query = { username: 'steve' }; var request = createRequestObject(query, route); request.route.response.failAction = 'log'; request.response = Hapi.Response.generate({ wrongParam: 'test' }); @@ -205,7 +205,7 @@ describe('Validation', function () { it('should raise an error when validating a non-object response', function (done) { - var query = { username: 'walmart' }; + var query = { username: 'steve' }; var request = createRequestObject(query, route); request.response = Hapi.Response.generate('test');