diff --git a/README.md b/README.md index 8317c204d..3eecbf6a9 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ infrastructure. The framework supports a powerful plugin architecture for pain-f For the latest updates and release information follow [@hapijs](https://twitter.com/hapijs) on twitter. -Current version: **5.1.x** ([release notes](https://github.com/spumko/hapi/issues?labels=release+notes&page=1&state=closed)) +Current version: **6.0.x** ([release notes](https://github.com/spumko/hapi/issues?labels=release+notes&page=1&state=closed)) [](http://travis-ci.org/spumko/hapi) diff --git a/docs/README.md b/docs/README.md index fdf6a78aa..114208c0a 100755 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,8 @@ ## Current Documentation - [v5.1.x](https://github.com/spumko/hapi/blob/master/docs/Reference.md) + [v6.0.x](https://github.com/spumko/hapi/blob/master/docs/Reference.md) ## Previous Documentation + [v5.1.x](https://github.com/spumko/hapi/blob/v5.1.0/docs/Reference.md) [v5.0.x](https://github.com/spumko/hapi/blob/v5.0.0/docs/Reference.md) [v4.1.x](https://github.com/spumko/hapi/blob/v4.1.0/docs/Reference.md) [v4.0.x](https://github.com/spumko/hapi/blob/v4.0.0/docs/Reference.md) diff --git a/docs/Reference.md b/docs/Reference.md index a549d8fa7..4545d67e5 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -1,4 +1,4 @@ -# 5.1.x API Reference +# 6.0.x API Reference - [`Hapi.Server`](#hapiserver) - [`new Server([host], [port], [options])`](#new-serverhost-port-options) @@ -23,12 +23,15 @@ - [`server.cache(name, options)`](#servercachename-options) - [`server.auth.scheme(name, scheme)`](#serverauthschemename-scheme) - [`server.auth.strategy(name, scheme, [mode], [options])`](#serverauthstrategyname-scheme-mode-options) + - [`server.auth.default(options)`](#serverauthdefault-options) + - [`server.auth.test(strategy, request, next)`](#serverauthteststrategy-request-next) - [`server.ext(event, method, [options])`](#serverextevent-method-options) - [Request lifecycle](#request-lifecycle) - [`server.method(name, fn, [options])`](#servermethodname-fn-options) - [`server.method(method)`](#servermethodmethod) - [`server.inject(options, callback)`](#serverinjectoptions-callback) - [`server.handler(name, method)`](#serverhandlername-method) + - [`server.location(uri, [request])`](#serverlocationuri-request) - [`Server` events](#server-events) - [Request object](#request-object) - [`request` properties](#request-properties) @@ -46,6 +49,7 @@ - [`reply.view(template, [context, [options]])`](#replyviewtemplate-context-options) - [`reply.close([options])`](#replycloseoptions) - [`reply.proxy(options)`](#replyproxyoptions) + - [`reply.redirect(location)`](#replyredirectlocation) - [Response object](#response-object) - [Response events](#response-events) - [`Hapi.error`](#hapierror) @@ -65,40 +69,30 @@ - [`pack.server([host], [port], [options])`](#packserverhost-port-options) - [`pack.start([callback])`](#packstartcallback) - [`pack.stop([options], [callback])`](#packstopoptions-callback) - - [`pack.require(name, options, callback)`](#packrequirename-options-callback) - - [`pack.require(names, callback)`](#packrequirenames-callback) - - [`pack.register(plugin, options, callback)`](#packregisterplugin-options-callback) -- [`Hapi.Composer`](#hapicomposer) - - [`new Composer(manifest)`](#new-composermanifest) - - [`composer.compose(callback)`](#composercomposecallback) - - [`composer.start([callback])`](#composerstartcallback) - - [`composer.stop([options], [callback])`](#composerstopoptions-callback) + - [`pack.register(plugins, options, callback)`](#packregisterplugins-options-callback) + - [`Pack.compose(manifest, [options], callback)`](#Packcomposemanifest-options-callback) - [Plugin interface](#plugin-interface) - [`exports.register(plugin, options, next)`](#exportsregisterplugin-options-next) - [Root methods and properties](#root-methods-and-properties) - - [`plugin.version`](#pluginversion) - - [`plugin.path`](#pluginpath) - [`plugin.hapi`](#pluginhapi) + - [`plugin.version`](#pluginversion) - [`plugin.app`](#pluginapp) - - [`plugin.events`](#pluginevents) - [`plugin.plugins`](#pluginplugins) + - [`plugin.path(path)`](#pluginpathpath) - [`plugin.log(tags, [data, [timestamp]])`](#pluginlogtags-data-timestamp) - - [`plugin.dependency(deps, [after])`](#plugindependencydeps-after) - [`plugin.after(method)`](#pluginaftermethod) - [`plugin.views(options)`](#pluginviewsoptions) - [`plugin.method(name, fn, [options])`](#pluginmethodname-fn-options) - [`plugin.method(method)`](#pluginmethodmethod) - [`plugin.methods`](#pluginmethods) - [`plugin.cache(options)`](#plugincacheoptions) - - [`plugin.require(name, options, callback)`](#pluginrequirename-options-callback) - - [`plugin.require(names, callback)`](#pluginrequirenames-callback) - - [`plugin.loader(require)`](#pluginloader-require) - [`plugin.bind(bind)`](#pluginbind-bind) - [`plugin.handler(name, method)`](#pluginhandlername-method) - [Selectable methods and properties](#selectable-methods-and-properties) - [`plugin.select(labels)`](#pluginselectlabels) - [`plugin.length`](#pluginlength) - [`plugin.servers`](#pluginservers) + - [`plugin.events`](#pluginevents) - [`plugin.expose(key, value)`](#pluginexposekey-value) - [`plugin.expose(obj)`](#pluginexposeobj) - [`plugin.route(options)`](#pluginrouteoptions) @@ -107,10 +101,12 @@ - [`plugin.auth.scheme(name, scheme)`](#pluginauthschemename-scheme) - [`plugin.auth.strategy(name, scheme, [mode], [options])`](#pluginauthstrategyname-scheme-mode-options) - [`plugin.ext(event, method, [options])`](#pluginextevent-method-options) + - [`plugin.register(plugins, options, callback)`](#pluginregisterplugins-options-callback) + - [`plugin.dependency(deps, [after])`](#plugindependencydeps-after) - [`Hapi.state`](#hapistate) - [`prepareValue(name, value, options, callback)`](#preparevaluename-value-options-callback) - [`Hapi.version`](#hapiversion) -- [Hapi CLI](#hapi-cli) +- [hapi CLI](#hapi-cli) ## `Hapi.Server` @@ -118,9 +114,10 @@ Creates a new server instance with the following arguments: -- `host` - the hostname, IP address, or path to UNIX domain socket the server is bound to. Defaults to `0.0.0.0` which means any available network - interface. Set to `127.0.0.1` or `localhost` to restrict connection to those coming from the same machine. If `host` contains a '/' character, it - is used as a UNIX domain socket path and if it starts with '\\.\pipe' as a Windows named pipe. +- `host` - the hostname, IP address, or path to UNIX domain socket the server is bound to. Defaults to `0.0.0.0` which + means any available network interface. Set to `127.0.0.1` or `localhost` to restrict connection to those coming from + the same machine. If `host` contains a '/' character, it is used as a UNIX domain socket path and if it starts with + '\\.\pipe' as a Windows named pipe. - `port` - the TCP port the server is listening to. Defaults to port `80` for HTTP and to `443` when TLS is configured. To use an ephemeral port, use `0` and once the server is started, retrieve the port allocation via `server.info.port`. - `options` - An object with the server configuration as described in [server options](#server-options). @@ -143,29 +140,32 @@ var server = Hapi.createServer('localhost', 8000, { cors: true }); When creating a server instance, the following options configure the server's behavior: -- `app` - application-specific configuration which can later be accessed via `server.settings.app`. Provides a safe place to store application configuration without potential conflicts with **hapi**. Should not be used by plugins which should use `plugins[name]`. Note the difference between - `server.settings.app` which is used to store configuration value and `server.app` which is meant for storing run-time state. +- `app` - application-specific configuration which can later be accessed via `server.settings.app`. Provides a safe + place to store application configuration without potential conflicts with **hapi**. Should not be used by plugins which + should use `plugins[name]`. Note the difference between `server.settings.app` which is used to store configuration value + and `server.app` which is meant for storing run-time state. -- `cache` - determines the type of server-side cache used. Every server includes a default cache for storing and - application state. By default, a simple memory-based cache is created which has a limited capacity. **hapi** uses - [**catbox** module documentation](https://github.com/spumko/catbox#client) as its cache implementation which includes support for Redis, MongoDB, - Memcached, and Riak. Caching is only utilized if methods and plugins explicitly store their state in the cache. The server cache configuration - only defines the store itself. `cache` can be assigned: +- `cache` - determines the type of server-side cache used. Every server includes a default + cache for storing and application state. By default, a simple memory-based cache is created which has a limited capacity. + **hapi** uses [**catbox** module documentation](https://github.com/spumko/catbox#client) as its cache implementation which + includes support for Redis, MongoDB, Memcached, and Riak. Caching is only utilized if methods and plugins explicitly store + their state in the cache. The server cache configuration only defines the store itself. `cache` can be assigned: - a string with the cache engine module name (e.g. `'catbox-memory'`, `'catbox-redis'`). - a configuration object with the following options: - `engine` - - cache options. The cache options are described in the [**catbox** module documentation](https://github.com/spumko/catbox#client). When an array - of options is provided, multiple cache connections are established and each array item (except one) must include an additional option: - - `name` - an identifier used later when provisioning or configuring caching for routes, methods, or plugins. Each connection name must be unique. A - single item may omit the `name` option which defines the default cache. If every connection includes a `name`, a default memory cache is provisions - as well as the default. - - `shared` - if `true`, allows multiple cache users to share the same segment (e.g. multiple servers in a pack using the same route and cache. - Default to not shared. + cache options. The cache options are described in the [**catbox** module documentation](https://github.com/spumko/catbox#client). + When an array of options is provided, multiple cache connections are established and each array item (except one) must + include an additional option: + - `name` - an identifier used later when provisioning or configuring caching for routes, methods, or plugins. Each + connection name must be unique. A single item may omit the `name` option which defines the default cache. If every + connection includes a `name`, a default memory cache is provisions as well as the default. + - `shared` - if `true`, allows multiple cache users to share the same segment (e.g. multiple servers in a pack using + the same route and cache. Default to not shared. - an array of the above types for configuring multiple cache instances, each with a unqiue name. -- `cors` - the [Cross-Origin Resource Sharing](http://www.w3.org/TR/cors/) protocol allows browsers to make cross-origin API calls. CORS is - required by web applications running inside a browser which are loaded from a different domain than the API server. CORS headers are disabled by - default. To enable, set `cors` to `true`, or to an object with the following options: +- `cors` - the [Cross-Origin Resource Sharing](http://www.w3.org/TR/cors/) protocol allows browsers to make cross-origin API + calls. CORS is required by web applications running inside a browser which are loaded from a different domain than the API + server. CORS headers are disabled by default. To enable, set `cors` to `true`, or to an object with the following options: - `origin` - a strings array of allowed origin servers ('Access-Control-Allow-Origin'). The array can contain any combination of fully qualified origins along with origin strings containing a wilcard '*' character, or a single `'*'` origin string. Defaults to any origin `['*']`. - `isOriginExposed` - if `false`, prevents the server from returning the full list of non-wildcard `origin` values if the incoming origin header @@ -268,9 +268,9 @@ When creating a server instance, the following options configure the server's be - `views` - enables support for view rendering (using templates to generate responses). Disabled by default. To enable, set to an object with the following options: - - `engines` - (required) an object where each key is a file extension (e.g. 'html', 'jade'), mapped to the npm module name (string) used for + - `engines` - (required) an object where each key is a file extension (e.g. 'html', 'jade'), mapped to the npm module used for rendering the templates. Alternatively, the extension can be mapped to an object with the following options: - - `module` - the npm module name (string) to require or an object with: + - `module` - the npm module used for rendering the templates. The module object must contain: - `compile()` - the rendering function. The required function signature depends on the `compileMode` settings. If the `compileMode` is `'sync'`, the signature is `compile(template, options)`, the return value is a function with signature `function(context, options)`, and the method is allowed to throw errors. If the `compileMode` is `'async'`, the signature is `compile(template, options, callback)` @@ -591,10 +591,9 @@ The following options are available when adding a route: - `expiresAt` - time of day expressed in 24h notation using the 'MM:HH' format, at which point all cache records for the route expire. Cannot be used together with `expiresIn`. - - `auth` - authentication configuration. Value can be: + - `auth` - authentication configuration. Value can be: + - `false` to disable authentication if a default strategy is set. - a string with the name of an authentication strategy registered with `server.auth.strategy()`. - - a boolean where `false` means no authentication, and `true` sets to the default authentication strategy which is available only - when a single strategy is configured. - an object with: - `mode` - the authentication mode. Defaults to `'required'` if a server authentication strategy is configured, otherwise defaults to no authentication. Available values: @@ -928,6 +927,9 @@ can be registered with the server using the `server.state()` method, where: - `password` - password used for HMAC key generation. - `password` - password used for `'iron'` encoding. - `iron` - options for `'iron'` encoding. Defaults to [`require('iron').defaults`](https://github.com/hueniverse/iron#options). + - `failAction` - overrides the default server `state.cookies.failAction` setting. + - `clearInvalid` - overrides the default server `state.cookies.clearInvalid` setting. + - `strictHeader` - overrides the default server `state.cookies.strictHeader` setting. ```javascript // Set cookie definition @@ -1043,6 +1045,22 @@ Registers an authentication strategy where: (`'required'`, `'optional'`, `'try'`). Defaults to `false`. - `options` - scheme options based on the scheme requirements. +#### `server.auth.default(options)` + +Sets a default startegy which is applied to every route. The default does not apply when the route config specifies `auth` as `false`, +or has an authentication strategy configured. Otherwise, the route authentication config is applied to the defaults. The function requires: +- `options` - a string with the default strategy name or an object with a specified strategy or strategies using the same format as the + [route `auth` handler options](#route.config.auth). + +#### `server.auth.test(strategy, request, next)` + +Tests a request against an authentication strategy where: +- `strategy` - the strategy name registered with `server.auth.strategy()`. +- `request` - the request object. The request route authentication configuration is not used. +- `next` - the callback function with signature `function(err, credentials)` where: + - `err` - the error if authentication failed. + - `credentials` - the authentication credentials object if authentication was successful. + #### `server.ext(event, method, [options])` Registers an extension function in one of the available [extension points](#request-lifecycle) where: @@ -1305,6 +1323,20 @@ server.route({ server.start(); ``` +#### `server.location(uri, [request])` + +Converts the provided URI to an absolute URI using the server or request configuration where: +- `uri` - the relative URI. +- `request` - an optional request object for using the request host header if no server location has been configured. + +```javascript +var Hapi = require('hapi'); +var server = Hapi.createServer('localhost', 8000); + +console.log(server.location('/relative')); +``` + + ### `Server` events The server object inherits from `Events.EventEmitter` and emits the following events: @@ -1618,7 +1650,7 @@ var pre = function (request, reply) { ### `reply([result])` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Concludes the handler activity by returning control over to the router where: @@ -1659,7 +1691,7 @@ Note that if `result` is a `Stream` with a `statusCode` property, that status co ### `reply.file(path, [options])` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Transmits a file from the file system. The 'Content-Type' header defaults to the matching mime type based on filename extension.: @@ -1686,7 +1718,7 @@ var handler = function (request, reply) { ### `reply.view(template, [context, [options]])` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Concludes the handler activity by returning control over to the router with a templatized view response where: @@ -1739,7 +1771,7 @@ server.route({ method: 'GET', path: '/', handler: handler }); ### `reply.close([options])` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Concludes the handler activity by returning control over to the router and informing the router that a response has already been sent back directly via `request.raw.res` and that no further response action is needed. Supports the following optional options: @@ -1752,7 +1784,7 @@ The [response flow control rules](#flow-control) **do not** apply. ### `reply.proxy(options)` _Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, -`reply.close()`, or `reply.proxy()` is called._ +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ Proxies the request to an upstream endpoint where: - `options` - an object including the same keys and restrictions defined by the [route `proxy` handler options](#route.config.proxy). @@ -1768,6 +1800,24 @@ var handler = function (request, reply) { }; ``` +### `reply.redirect(location)` + +_Available only within the handler method and only before one of `reply()`, `reply.file()`, `reply.view()`, +`reply.close()`, `reply.proxy()`, or `reply.redirect()` is called._ + +Redirects the client to the specified location. Same as calling `reply().redirect(location)`. + +Returns a response object. + +The [response flow control rules](#flow-control) apply. + +```javascript +var handler = function (request, reply) { + + reply.redirect('http://example.com'); +}; +``` + ## Response object Every response includes the following properties: @@ -2076,7 +2126,7 @@ Creates a new `Pack` object instance where: - `options` - optional configuration: - `app` - an object used to initialize the application-specific data stored in `pack.app`. - `cache` - cache configuration as described in the server [`cache`](#server.config.cache) option. - - `requirePath` - sets the path from which node module plugins are loaded. Applies only when using [`pack.require()`](#packrequirename-options-callback) + - `requirePath` - sets the path from which node module plugins are loaded. Applies only when using [`pack.register()`](#packregisterplugins-options-callback) with module names that do no include a relative or absolute path (e.g. 'lout'). Defaults to the node module behaviour described in [node modules](http://nodejs.org/api/modules.html#modules_loading_from_node_modules_folders). Note that if the modules are located inside a 'node_modules' sub-directory, `requirePath` must end with `'/node_modules'`. @@ -2094,11 +2144,8 @@ Each `Pack` object instance has the following properties: Initialized via the pack `app` configuration option. Defaults to `{}`. - `events` - an `Events.EventEmitter` providing a consolidate emitter of all the events emitted from all member pack servers as well as the `'start'` and `'stop'` pack events. -- `list` - an object listing all the registered plugins where each key is a plugin name and the value is an object with: - - `name` - plugin name. - - `version` - plugin version. - - `path` - the plugin root path (where 'package.json' is located). - - `register()` - the [`exports.register()`](#exportsregisterplugin-options-next) function. +- `plugins` - an object where each key is a plugin name and the value are the exposed properties by that plugin using + [`plugin.expose()`](#pluginexposekey-value). ### `Pack` methods @@ -2144,85 +2191,67 @@ pack.stop({ timeout: 60 * 1000 }, function () { }); ``` -#### `pack.require(name, [options], callback)` +#### `pack.register(plugins, [options], callback)` Registers a plugin where: -- `name` - the node module name as expected by node's [`require()`](http://nodejs.org/api/modules.html#modules_module_require_id). If `name` is a relative - path it is relative to the location of the file requiring it. If `name` is not a relative or absolute path (e.g. 'furball'), it is prefixed with the - value of the pack `requirePath` configuration option when present. Note that node's `require()` is invoked by hapi which means, the `'node_modules'` path - is relative to the location of the hapi module. -- `options` - optional configuration object which is passed to the plugin via the `options` argument in - [`exports.register()`](#exportsregisterplugin-options-next). +- `plugins` - a plugin object or array of plugin objects. The objects can use one of two formats: + - a module plugin object. + - a manually constructed plugin object. +- `options` - optional registration options (used by **hapi** and is not passed to the plugin): + - `select` - string or array of strings of labels to pre-select for plugin registration. + - `route` - apply modifiers to any routes added by the plugin: + - `prefix` - string added as prefix to any route path (must begin with `'/'`). If a plugin registers a child plugin + the `prefix` is passed on to the child or is added in front of the child-specific prefix. + - `vhost` - virtual host string (or array of strings) applied to every route. The outter-most `vhost` overrides the any + nested configuration. - `callback` - the callback function with signature `function(err)` where: - - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts - (e.g. among routes, methods, state) will throw an error and will not return a callback. - -```javascript -pack.require('furball', { version: '/v' }, function (err) { - - if (err) { - console.log('Failed loading plugin: furball'); - } -}); -``` - -#### `pack.require(names, callback)` - -Registers a list of plugins where: - -- `names` - an array of plugins names as described in [`pack.require()`](#packrequirename-options-callback), or an object in which - each key is a plugin name, and each value is the `options` object used to register that plugin. -- `callback` - the callback function with signature `function(err)` where: - - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts - (e.g. among routes, methods, state) will throw an error and will not return a callback. + - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts + (e.g. among routes, methods, state) will throw an error and will not return a callback. -Batch registration is required when plugins declare a [dependency](#plugindependencydeps-after), so that all the required dependencies are loaded in -a single transaction (internal order does not matter). +Module plugin is registered by passing the following object (or array of object) as `plugins`: +- `plugin` - an object (usually obtained by calling node's `require()`) with: + - `register` - the [`exports.register()`](#exportsregisterplugin-options-next) function. The function must have an `attributes` + property with either `name` (and optional `version`) keys or `pkg` with the content of the module's 'package.json'. +- `options` - optional configuration object which is passed to the plugin via the `options` argument in + [`exports.register()`](#exportsregisterplugin-options-next). ```javascript -pack.require(['furball', 'lout'], function (err) { - - if (err) { - console.log('Failed loading plugin: furball'); +server.pack.register({ + plugin: require('plugin_name'), + options: { + message: 'hello' } -}); - -pack.require({ furball: null, lout: { endpoint: '/docs' } }, function (err) { + }, function (err) { - if (err) { - console.log('Failed loading plugins'); - } -}); + if (err) { + console.log('Failed loading plugin'); + } + }); ``` -#### `pack.register(plugin, options, callback)` - -Registers a plugin object (without using `require()`) where: - -- `plugin` - the plugin object which requires: - - `name` - plugin name. - - `version` - plugin version. - - `path` - optional plugin path for resolving relative paths used by the plugin. Defaults to current working directory. - - `register()` - the [`exports.register()`](#exportsregisterplugin-options-next) function. +Manually constructed plugin is an object containing: +- `name` - plugin name. +- `version` - an optional plugin version. Defaults to `'0.0.0'`. +- `multiple` - an optional boolean indicating if the plugin is safe to register multiple time with the same server. + Defaults to `false`. +- `register` - the [`register()`](#exportsregisterplugin-options-next) function. - `options` - optional configuration object which is passed to the plugin via the `options` argument in [`exports.register()`](#exportsregisterplugin-options-next). -- `callback` - the callback function with signature `function(err)` where: - - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts - (e.g. among routes, methods, state) will throw an error and will not return a callback. ```javascript -var plug = { +server.pack.register({ name: 'test', version: '2.0.0', register: function (plugin, options, next) { - plugin.route({ method: 'GET', path: '/special', handler: function (request, reply) { reply(options.message); } } ); + plugin.route({ method: 'GET', path: '/special', handler: function (request, reply) { reply(options.message); } }); next(); + }, + options: { + message: 'hello' } -}; - -server.pack.register(plug, { message: 'hello' }, function (err) { +}, function (err) { if (err) { console.log('Failed loading plugin'); @@ -2230,21 +2259,32 @@ server.pack.register(plug, { message: 'hello' }, function (err) { }); ``` -## `Hapi.Composer` - -The `Composer` provides a simple way to construct a [`Pack`](#hapipack) from a single configuration object, including configuring servers -and registering plugins. - -#### `new Composer(manifest)` +### `Pack.compose(manifest, [options], callback)` -Creates a `Composer` object instance where: +Provides a simple way to construct a [`Pack`](#hapipack) from a single configuration object, including configuring servers +and registering plugins where: -- `manifest` - an object or array or objects where: +- `manifest` - an object with the following keys: - `pack` - the pack `options` as described in [`new Pack()`](#packserverhost-port-options). - `servers` - an array of server configuration objects where: - `host`, `port`, `options` - the same as described in [`new Server()`](#new-serverhost-port-options) with the exception that the - `cache` option is not allowed and must be configured via the pack `cache` option. The `host` and `port` keys can be set to an environment variable by prefixing the variable name with `'$env.'`. - - `plugin` - an object where each key is a plugin name, and each value is the `options` object used to register that plugin. + `cache` option is not allowed and must be configured via the pack `cache` option. The `host` and `port` keys can be set to an + environment variable by prefixing the variable name with `'$env.'`. + - `plugins` - an object where each key is a plugin name, and each value is one of: + - the `options` object passed to the plugin on registration. + - an array of object where: + - `options` - the object passed to the plugin on registration. + - any key supported by the `pack.register()` options used for registration (e.g. `select`). +- `options` - optional compose configuration: + - `relativeTo` - path prefix used when loading plugins using node's `require()`. The `relativeTo` path prefix is added to any + relative plugin name (i.e. beings with `'./'`). All other module names are required as-is and will be looked up from the location + of the **hapi** module path (e.g. if **hapi** resides outside of your project `node_modules` path, it will not find your project + dependencies - you should specify them as relative and use the `relativeTo` option). +- `callback` - the callback method, called when all packs and servers have been created and plugins registered has the signature + `function(err, pack)` where: + - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts + (e.g. among routes, methods, state) will throw an error and will not return a callback. + - `pack` - the composed Pack object. ```javascript var Hapi = require('hapi'); @@ -2273,154 +2313,80 @@ var manifest = { cookieOptions: { password: 'secret' } - } + }, + 'furball': [ + { + select: 'web', + options: { + version: '/v' + } + } + ] } }; -var composer = new Hapi.Composer(manifest); -``` - -#### `composer.compose(callback)` - -Creates the packs described in the manifest construction where: +Hapi.Pack.composer(manifest, function (err, pack) { -- `callback` - the callback method, called when all packs and servers have been created and plugins registered has the signature - `function(err)` where: - - `err` - an error returned from `exports.register()`. Note that incorrect usage, bad configuration, or namespace conflicts - (e.g. among routes, methods, state) will throw an error and will not return a callback. - -```javascript -composer.compose(function (err) { - - if (err) { - console.log('Failed composing'); - } + pack.start(); }); ``` -#### `composer.start([callback])` +## Plugin interface -Starts all the servers in all the pack composed where: +Plugins provide an extensibility platform for both general purpose utilities such as [batch requests](https://github.com/spumko/bassmaster) +and for application business logic. Instead of thinking about a web server as a single entity with a unified routing table, plugins enable +developers to break their application into logical units, assembled together in different combinations to fit the development, testing, and +deployment needs. -- `callback` - the callback method called when all the servers have been started. +A plugin is constructed with the following: -```javascript -composer.start(function () { +- name - the plugin name is used as a unique key. Public plugins should be published in the [npm registry](https://npmjs.org) and derive + their name from the registry name to ensure uniqueness. Private plugin names should be picked carefully to avoid conflicts with both + private and public names. +- registeration function - the function described in [`exports.register()`](#exportsregisterplugin-options-next) is the plugin's core. + The function is called when the plugin is registered and it performs all the activities required by the plugin to operate. It is the + single entry point into the plugin's functionality. +- version - the optional plugin version is only used informatively to enable other plugins to find out the verions loaded. The version + should be the same as the one specified in the plugin's 'package.json' file. - console.log('All servers started'); -}); -``` - -#### `composer.stop([options], [callback])` - -Stops all the servers in all the packs and used as described in [`server.stop([options], [callback])`](#serverstopoptions-callback). +The name and versions are included by attaching an `attributes` property to the `exports.register()` function: ```javascript -pack.stop({ timeout: 60 * 1000 }, function () { +exports.register = function (plugin, options, next) { - console.log('All servers stopped'); -}); -``` + plugin.route({ + method: 'GET', + path: '/version', + handler: function (request, reply) { -## Plugin interface + reply('1.0.0'); + } + }); -Plugins provide an extensibility platform for both general purpose utilities such as [batch requests](https://github.com/spumko/bassmaster) and for -application business logic. Instead of thinking about a web server as a single entity with a unified routing table, plugins enable developers to -break their application into logical units, assembled together in different combinations to fit the development, testing, and deployment needs. - -Constructing a plugin requires the following: - -- name - the plugin name is used as a unique key. Public plugins should be published in the [npm registry](https://npmjs.org) and derive their name - from the registry name. This ensures uniqueness. Private plugin names should be picked carefully to avoid conflicts with both private and public - names. Typically, private plugin names use a prefix such as the company name or an unusual combination of characters (e.g. `'--'`). When using the - [`pack.require()`](#packrequirename-options-callback) interface, the name is obtained from the 'package.json' module file. When using the - [`pack.register()`](#packregisterplugin-options-callback) interface, the name is provided as a required key in `plugin`. -- version - the plugin version is only used informatively within the framework but plays an important role in the plugin ecosystem. The plugin - ecosystem relies on the [npm peer dependency](http://blog.nodejs.org/2013/02/07/peer-dependencies/) functionality to ensure that plugins can - specify their dependency on a specific version of **hapi**, as well as on each other. Dependencies are expressed solely within the 'package.json' - file, and are enforced by **npm**. When using the [`pack.require()`](#packrequirename-options-callback) interface, the version is obtained from - the 'package.json' module file. When using the [`pack.register()`](#packregisterplugin-options-callback) interface, the version is provided as - a required key in `plugin`. -- `exports.register()` - the registration function described in [`exports.register()`](#exportsregisterplugin-options-next) is the plugin's core. - The function is called when the plugin is registered and it performs all the activities required by the plugin to operate. It is the single entry - point into the plugin functionality. When using the [`pack.require()`](#packrequirename-options-callback) interface, the function is obtained by - [`require()`](http://nodejs.org/api/modules.html#modules_module_require_id)'ing the plugin module and invoking the exported `register()` method. - When using the [`pack.register()`](#packregisterplugin-options-callback) interface, the function is provided as a required key in `plugin`. - -**package.json** - -```json -{ - "name": "furball", - "description": "Plugin utilities and endpoints", - "version": "0.3.0", - "main": "index", - "dependencies": { - "hoek": "0.8.x" - }, - "peerDependencies": { - "hapi": "1.x.x" - } -} + next(); +}; + +exports.register.attributes = { + name: 'example', + version: '1.0.0' +}; ``` -**index.js** +Alternatively, the name and version can be included via the `pkg` attribute containing the 'package.json' file for the module which +already has the name and version included: ```javascript -var Hoek = require('hoek'); - -var internals = { - defaults: { - version: '/version', - plugins: '/plugins' - } +exports.register.attributes = { + pkg: require('./package.json') }; +``` -internals.version = 1.1; - -exports.register = function (plugin, options, next) { - - var settings = Hoek.applyToDefaults(internals.defaults, options); - - if (settings.version) { - plugin.route({ - method: 'GET', - path: settings.version, - handler: function (request, reply) { - - reply(internals.version); - } - }); - } - - if (settings.plugins) { - plugin.route({ - method: 'GET', - path: settings.plugins, - handler: function (request, reply) { - - reply(listPlugins(request.server)); - } - }); - } - - var listPlugins = function (server) { - - var plugins = []; - Object.keys(server.pack.list).forEach(function (name) { - - var item = server.pack.list[name]; - plugins.push({ - name: item.name, - version: item.version - }); - }); +The `multiple` attributes specifies that a plugin is safe to register multiple times with the same server. - return plugins; - }; - - plugin.expose('plugins', listPlugins); - next(); +```javascript +exports.register.attributes = { + multiple: true, + pkg: require('./package.json') }; ``` @@ -2449,32 +2415,6 @@ The plugin interface root methods and properties are those available only on the [`exports.register()`](#exportsregisterplugin-options-next) interface. They are not available on the object received by calling [`plugin.select()`](#pluginselectlabels). -#### `plugin.version` - -The plugin version information. - -```javascript -exports.register = function (plugin, options, next) { - - console.log(plugin.version); - next(); -}; -``` - -#### `plugin.path` - -The plugin root path (where 'package.json' resides). - -```javascript -var Fs = require('fs'); - -exports.register = function (plugin, options, next) { - - var file = Fs.readFileSync(plugin.path + '/resources/image.png'); - next(); -}; -``` - #### `plugin.hapi` A reference to the **hapi** module used to create the pack and server instances. Removes the need to add a dependency on **hapi** within the plugin. @@ -2494,30 +2434,26 @@ exports.register = function (plugin, options, next) { }; ``` -#### `plugin.app` +#### `plugin.version` -Provides access to the [common pack application-specific state](#pack-properties). +The **hapi** version used to load the plugin. ```javascript exports.register = function (plugin, options, next) { - plugin.app.hapi = 'joi'; + console.log(plugin.version); next(); }; ``` -#### `plugin.events` +#### `plugin.app` -The `pack.events' emitter. +Provides access to the [common pack application-specific state](#pack-properties). ```javascript exports.register = function (plugin, options, next) { - plugin.events.on('internalError', function (request, err) { - - console.log(err); - }); - + plugin.app.hapi = 'joi'; next(); }; ``` @@ -2535,44 +2471,29 @@ exports.register = function (plugin, options, next) { }; ``` -#### `plugin.log(tags, [data, [timestamp]])` +#### `plugin.path(path)` -Emits a `'log'` event on the `pack.events` emitter using the same interface as [`server.log()`](#serverlogtags-data-timestamp). +Sets the path prefix used to locate static resources (files and view templates) when relative paths are used by the plugin: +- `path` - the path prefix added to any relative file path starting with `'.'`. The value has the same effect as using the server's + configuration `files.relativeTo` option but only within the plugin. ```javascript exports.register = function (plugin, options, next) { - plugin.log(['plugin', 'info'], 'Plugin registered'); + plugin.path(__dirname + '../static'); + plugin.route({ path: '/file', method: 'GET', handler: { file: './test.html' } }); next(); }; ``` -#### `plugin.dependency(deps, [after])` - -Declares a required dependency upon other plugins where: +#### `plugin.log(tags, [data, [timestamp]])` -- `deps` - a single string or array of strings of plugin names which must be registered in order for this plugin to operate. Plugins listed - must be registered in the same pack transaction to allow validation of the dependency requirements. Does not provide version dependency which - should be implemented using [npm peer dependencies](http://blog.nodejs.org/2013/02/07/peer-dependencies/). -- `after` - an optional function called after all the specified dependencies have been registered and before the servers start. The function is only - called if the pack servers are started. If a circular dependency is created, the call will assert (e.g. two plugins each has an `after` function - to be called after the other). The function signature is `function(plugin, next)` where: - - `plugin` - the [plugin interface](#plugin-interface) object. - - `next` - the callback function the method must call to return control over to the application and complete the registration process. The function - signature is `function(err)` where: - - `err` - internal plugin error condition, which is returned back via the [`pack.start(callback)`](#packstartcallback) callback. A plugin - registration error is considered an unrecoverable event which should terminate the application. +Emits a `'log'` event on the `pack.events` emitter using the same interface as [`server.log()`](#serverlogtags-data-timestamp). ```javascript exports.register = function (plugin, options, next) { - plugin.dependency('yar', after); - next(); -}; - -var after = function (plugin, next) { - - // Additional plugin registration logic + plugin.log(['plugin', 'info'], 'Plugin registered'); next(); }; ``` @@ -2709,51 +2630,6 @@ exports.register = function (plugin, options, next) { }; ``` -#### `plugin.require(name, [options], callback)` - -Registers a plugin with the same pack as the current plugin following the syntax of [`pack.require()`](#packrequirename-options-callback). - -```javascript -exports.register = function (plugin, options, next) { - - plugin.require('furball', { version: '/v' }, function (err) { - - next(err); - }); -}; -``` - -#### `plugin.require(names, callback)` - -Registers a list of plugins with the same pack following the syntax of [`pack.require()`](#packrequirename-callback). - -```javascript -exports.register = function (plugin, options, next) { - - plugin.require(['furball', 'lout'], function (err) { - - next(err); - }); -}; -``` - -#### `plugin.loader(require)` - -Forces using the local `require()` method provided by node when calling `plugin.require()`. This sets the module path relative -to the plugin instead of relative to the hapi framework module location. This is needed to work around the limitations in node's -`require()`. - -```javascript -exports.register = function (plugin, options, next) { - - plugin.loader(require); - plugin.require('furball', { version: '/v' }, function (err) { - - next(err); - }); -}; -``` - #### `plugin.bind(bind)` Sets a global plugin bind used as the default bind when adding a route or an extension using the plugin interface (if no @@ -2853,6 +2729,22 @@ exports.register = function (plugin, options, next) { }; ``` +#### `plugin.events` + +An emitter containing the events of all the selected servers. + +```javascript +exports.register = function (plugin, options, next) { + + plugin.events.on('internalError', function (request, err) { + + console.log(err); + }); + + next(); +}; +``` + #### `plugin.expose(key, value)` Exposes a property via `plugin.plugins[name]` (if added to the plugin root without first calling `plugin.select()`) and `server.plugins[name]` @@ -2951,6 +2843,45 @@ exports.register = function (plugin, options, next) { }; ``` +#### `plugin.require(plugins, [options], callback)` + +Registers a plugin with the current selection following the syntax of `pack.register()`. + +exports.register = function (plugin, options, next) { + + plugin.register({ plugin: require('furball'), options: { version: '/v' } }, next); +}; + +#### `plugin.dependency(deps, [after])` + +Declares a required dependency upon other plugins where: + +- `deps` - a single string or array of strings of plugin names which must be registered in order for this plugin to operate. Plugins listed + must be registered in the same pack transaction to allow validation of the dependency requirements. Does not provide version dependency which + should be implemented using [npm peer dependencies](http://blog.nodejs.org/2013/02/07/peer-dependencies/). +- `after` - an optional function called after all the specified dependencies have been registered and before the servers start. The function is only + called if the pack servers are started. If a circular dependency is created, the call will assert (e.g. two plugins each has an `after` function + to be called after the other). The function signature is `function(plugin, next)` where: + - `plugin` - the [plugin interface](#plugin-interface) object. + - `next` - the callback function the method must call to return control over to the application and complete the registration process. The function + signature is `function(err)` where: + - `err` - internal plugin error condition, which is returned back via the [`pack.start(callback)`](#packstartcallback) callback. A plugin + registration error is considered an unrecoverable event which should terminate the application. + +```javascript +exports.register = function (plugin, options, next) { + + plugin.dependency('yar', after); + next(); +}; + +var after = function (plugin, next) { + + // Additional plugin registration logic + next(); +}; +``` + ## `Hapi.state` #### `prepareValue(name, value, options, callback)` @@ -3006,18 +2937,20 @@ var Hapi = require('hapi'); console.log(Hapi.version); ``` -## `Hapi CLI` +## `hapi CLI` -The **hapi** command line interface allows a pack of servers to be composed and started from a configuration file only from the command line. -When installing **hapi** with the global flag the **hapi** binary script will be installed in the path. The following arguments are available to the -**hapi** CLI: +The **hapi** command line interface allows a pack of servers to be composed and started from a configuration file +only from the command line. When installing **hapi** with the global flag the **hapi** binary script will be +installed in the path. The following arguments are available to the **hapi** CLI: - '-c' - the path to configuration json file (required) - '-p' - the path to the node_modules folder to load plugins from (optional) - '--require' - a module the cli will require before hapi is required (optional) ex. loading a metrics library -Note that `--require` will require from node_modules, an absolute path, a relative path, or from the node_modules set by `-p` if available. - -In order to help with A/B testing there is [confidence](https://github.com/spumko/confidence). Confidence is a configuration document format, an API, and a foundation for A/B testing. The configuration format is designed to work with any existing JSON-based configuration, serving values based on object path ('/a/b/c' translates to a.b.c). In addition, confidence defines special $-prefixed keys used to filter values for a given criteria. - +Note that `--require` will require from node_modules, an absolute path, a relative path, or from the node_modules +set by `-p` if available. +In order to help with A/B testing there is [confidence](https://github.com/spumko/confidence). Confidence is a +configuration document format, an API, and a foundation for A/B testing. The configuration format is designed to +work with any existing JSON-based configuration, serving values based on object path ('/a/b/c' translates to a.b.c). +In addition, confidence defines special $-prefixed keys used to filter values for a given criteria. diff --git a/examples/auth.js b/examples/auth.js index 5838d32f5..770a1be4f 100755 --- a/examples/auth.js +++ b/examples/auth.js @@ -67,7 +67,7 @@ internals.handler = function (request, reply) { internals.main = function () { var server = new Hapi.Server(8000); - server.pack.require(['hapi-auth-basic', 'hapi-auth-hawk'], function (err) { + server.pack.register([require('hapi-auth-basic'), require('hapi-auth-hawk')], function (err) { server.auth.strategy('hawk', 'hawk', { getCredentialsFunc: internals.getCredentials }); server.auth.strategy('basic', 'basic', { validateFunc: internals.validate }); diff --git a/lib/auth.js b/lib/auth.js index a4952016d..855409ca7 100755 --- a/lib/auth.js +++ b/lib/auth.js @@ -4,6 +4,7 @@ var Boom = require('boom'); var Hoek = require('hoek'); var Semver = require('semver'); var Handler = require('./handler'); +var Schema = require('./schema'); // Declare internals @@ -16,10 +17,7 @@ exports = module.exports = internals.Auth = function (server) { this.server = server; this._schemes = {}; this._strategies = {}; - this._defaultStrategy = { // Strategy used as default if route has no auth settings - name: null, - mode: 'required' - }; + this._defaultStrategy = null; // Strategy used as default if route has no auth settings }; @@ -44,19 +42,57 @@ internals.Auth.prototype.strategy = function (name, scheme /*, mode, options */) Hoek.assert(!this._strategies[name], 'Authentication strategy name already exists'); Hoek.assert(scheme, 'Authentication strategy', name, 'missing scheme'); Hoek.assert(this._schemes[scheme], 'Authentication strategy', name, 'uses unknown scheme:', scheme); - Hoek.assert(!mode || !this._defaultStrategy.name, 'Cannot set default required strategy more than once:', name, '- already set to:', this._defaultStrategy); this._strategies[name] = this._schemes[scheme](this.server, options); if (mode) { - this._defaultStrategy.name = name; - this._defaultStrategy.mode = (typeof mode === 'string' ? mode : 'required'); - Hoek.assert(['required', 'optional', 'try'].indexOf(this._defaultStrategy.mode) !== -1, 'Unknown default authentication mode:', this._defaultStrategy.mode); + this.default({ strategies: [name], mode: mode === true ? 'required' : mode }); + } +}; + + +internals.Auth.prototype.default = function (options) { + + Schema.assert('auth', options, 'default strategy'); + Hoek.assert(!this._defaultStrategy, 'Cannot set default strategy more than once'); + + var settings = Hoek.clone(options); // options can be reused + + if (typeof settings === 'string') { + settings = { + strategies: [settings], + mode: 'required' + }; } + + if (settings.strategy) { + settings.strategies = [settings.strategy]; + delete settings.strategy; + } + + Hoek.assert(settings.strategies && settings.strategies.length, 'Default authentication strategy missing strategy name'); + + this._defaultStrategy = settings; +}; + + +internals.Auth.prototype.test = function (name, request, next) { + + Hoek.assert(name, 'Missing authentication strategy name'); + var strategy = this._strategies[name]; + Hoek.assert(strategy, 'Unknown authentication strategy:', name); + + var root = function (err, result) { + + return (err ? reply._root(err) : next(err, result.credentials)); + }; + + var reply = Handler.replyInterface(request, next, root); + strategy.authenticate.call(null, request, reply); }; -internals.Auth.prototype._setupRoute = function (options) { +internals.Auth.prototype._setupRoute = function (options, path) { var self = this; @@ -64,38 +100,36 @@ internals.Auth.prototype._setupRoute = function (options) { return options; // Preseve the difference between undefined and false } - var names = Object.keys(this._strategies); - var defaultName = (names.length === 1 ? names[0] : null); - if (typeof options === 'string') { options = { strategies: [options] }; } - else if (options === true) { - Hoek.assert(defaultName, 'Cannot set auth to true when more than one configured'); - options = { strategies: [defaultName] }; + + if (options.strategy) { + options.strategies = [options.strategy]; + delete options.strategy; } - options.mode = options.mode || 'required'; - Hoek.assert(['required', 'optional', 'try'].indexOf(options.mode) !== -1, 'Unknown authentication mode:', options.mode); + if (!options.strategies) { + Hoek.assert(this._defaultStrategy, 'Route missing authentication strategy and no default defined:', path); + options = Hoek.applyToDefaults(this._defaultStrategy, options); + } - Hoek.assert(!options.entity || ['user', 'app', 'any'].indexOf(options.entity) !== -1, 'Unknown authentication entity type:', options.entity); - Hoek.assert(!options.payload || ['required', 'optional'].indexOf(options.payload) !== -1, 'Unknown authentication payload mode:', options.entity); - Hoek.assert(!(options.strategy && options.strategies), 'Route can only have a auth.strategy or auth.strategies (or use the default) but not both'); - Hoek.assert(!options.strategies || options.strategies.length, 'Cannot have empty auth.strategies array'); - Hoek.assert(options.strategies || options.strategy || defaultName, 'Cannot use default strategy when none or more than one configured'); - options.strategies = options.strategies || [options.strategy || defaultName]; - delete options.strategy; + Hoek.assert(options.strategies.length, 'Route missing authentication strategy:', path); + options.mode = options.mode || 'required'; options.payload = options.payload || false; - var hasAuthenticatePayload = false; - options.strategies.forEach(function (strategy) { - Hoek.assert(self._strategies[strategy], 'Unknown authentication strategy:', strategy); - hasAuthenticatePayload = hasAuthenticatePayload || typeof self._strategies[strategy].payload === 'function'; - Hoek.assert(options.payload !== 'required' || hasAuthenticatePayload, 'Payload validation can only be required when all strategies support it'); - }); + if (options.payload) { + var hasAuthenticatePayload = false; + options.strategies.forEach(function (strategy) { - Hoek.assert(!options.payload || hasAuthenticatePayload, 'Payload authentication requires at least one strategy with payload support'); + Hoek.assert(self._strategies[strategy], 'Unknown authentication strategy:', strategy, 'in path:', path); + hasAuthenticatePayload = hasAuthenticatePayload || typeof self._strategies[strategy].payload === 'function'; + Hoek.assert(options.payload !== 'required' || hasAuthenticatePayload, 'Payload validation can only be required when all strategies support it in path:', path); + }); + + Hoek.assert(hasAuthenticatePayload, 'Payload authentication requires at least one strategy with payload support in path:', path); + } return options; }; @@ -103,24 +137,11 @@ internals.Auth.prototype._setupRoute = function (options) { internals.Auth.prototype._routeConfig = function (request) { - if (request.route.auth) { - return request.route.auth; - } - - if (request.route.auth === false || - request.route.auth === null) { - + if (request.route.auth === false) { return false; } - if (this._defaultStrategy.name) { - return { - mode: this._defaultStrategy.mode, - strategies: [this._defaultStrategy.name] - }; - } - - return false; + return request.route.auth || this._defaultStrategy; }; @@ -185,7 +206,7 @@ internals.Auth.prototype._authenticate = function (request, next) { }); }; - var validate = function (err, strategy, result) { + var validate = function (err, strategy, result) { // err can be Boom, Error, or a valid response object if (!strategy) { return next(err); @@ -204,7 +225,8 @@ internals.Auth.prototype._authenticate = function (request, next) { request.log(['hapi', 'auth', 'error', strategy].concat(result.log.tags), result.log.data); } else { - request.log(['hapi', 'auth', 'error', 'unauthenticated'], (err.isBoom ? err : err.statusCode)); + var tags = err.isBoom ? ['hapi', 'auth', 'error', 'unauthenticated'] : ['hapi', 'auth', 'response', 'unauthenticated']; + request.log(tags, (err.isBoom ? err : err.statusCode)); } if (err.isMissing) { @@ -220,7 +242,7 @@ internals.Auth.prototype._authenticate = function (request, next) { request.auth.strategy = strategy; request.auth.credentials = result.credentials; request.auth.artifacts = result.artifacts; - request.log(['hapi', 'auth', 'error', 'unauthenticated', 'try'], err); + request.log(['hapi', 'auth', 'unauthenticated', 'try'], err); return next(); } diff --git a/lib/cli.js b/lib/cli.js index b743a5625..35f87805e 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -54,9 +54,8 @@ internals.loadExtras = function () { console.error('Unable to require extra file: %s (%s)', extras, err.message); process.exit(1); } +}; - return; -} internals.getManifest = function () { @@ -72,7 +71,7 @@ internals.getManifest = function () { } return manifest; -} +}; internals.loadPacks = function (manifest, callback) { @@ -89,71 +88,55 @@ internals.loadPacks = function (manifest, callback) { return callback(err); } - options.pack = { requirePath: path }; + options = { relativeTo: path }; callback(null, options); }); -} - - -internals.createComposer = function (manifest, options) { - - var attached = !!internals.composer; // When composer exists events are already attached. - internals.composer = new Hapi.Composer(manifest, options); - - internals.composer.compose(function (err) { - - Hoek.assert(!err, 'Failed loading plugins: ' + (err && err.message)); - internals.composer.start(function (err) { - - Hoek.assert(!err, 'Failed starting server: ' + (err && err.message)); - - if (!attached) { - internals.attachEvents(); - } - }); - }); }; -internals.attachEvents = function () { +exports.start = function () { - process.once('SIGQUIT', internals.stop); // Use kill -s QUIT {pid} to kill the servers gracefully - process.on('SIGUSR2', internals.restart); // Use kill -s SIGUSR2 {pid} to restart the servers -}; + internals.loadExtras(); + Hapi = require('..') + var manifest = internals.getManifest(); + + internals.loadPacks(manifest, function (err, options) { + if (err) { + console.error(err); + process.exit(1); + } -internals.stop = function () { + Hapi.Pack.compose(manifest, options, function (err, pack) { - internals.composer.stop(function () { + Hoek.assert(!err, 'Failed loading plugins: ' + (err && err.message)); - process.exit(0); - }); -}; + pack.start(function (err) { + Hoek.assert(!err, 'Failed starting server: ' + (err && err.message)); -internals.restart = function () { + // Use kill -s QUIT {pid} to kill the servers gracefully - console.log('Stopping...'); - internals.composer.stop(function () { + process.once('SIGQUIT', function () { - console.log('Starting...'); - internals.start(); - }); -}; + pack.stop(function () { + process.exit(0); + }); + }); -exports.start = function () { + // Use kill -s SIGUSR2 {pid} to restart the servers - internals.loadExtras(); - Hapi = require('..') - var manifest = internals.getManifest(); - internals.loadPacks(manifest, function (err, options) { + process.on('SIGUSR2', function () { - if (err) { - console.error(err); - process.exit(1); - } + console.log('Stopping...'); + pack.stop(function () { - internals.createComposer(manifest, options); + console.log('Starting...'); + internals.start(); + }); + }); + }); + }); }); }; diff --git a/lib/composer.js b/lib/composer.js deleted file mode 100755 index be0305c18..000000000 --- a/lib/composer.js +++ /dev/null @@ -1,133 +0,0 @@ -// Load modules - -var Async = require('async'); -var Hoek = require('hoek'); -var Pack = require('./pack'); - - -// Declare internals - -var internals = {}; - -/* -var config = [{ - pack: { - cache: 'redis', - app: { - 'app-specific': 'value' - } - }, - servers: [ - { - port: 8001, - options: { - labels: ['api', 'nasty'] - } - }, - { - host: 'localhost', - port: '$env.PORT', - options: { - labels: ['api', 'nice'] - } - } - ], - plugins: { - furball: { - version: false, - plugins: '/' - } - } -}]; -*/ - -exports = module.exports = internals.Composer = function (manifest, options) { - - this._manifest = Hoek.clone([].concat(manifest)); - this._settings = Hoek.clone(options || {}); - this._packs = []; -}; - - -internals.Composer.prototype.compose = function (callback) { - - var self = this; - - // Create packs - - var sets = []; - this._manifest.forEach(function (set) { - - Hoek.assert(set.servers && set.servers.length, 'Pack missing servers definition'); - Hoek.assert(set.plugins, 'Pack missing plugins definition'); - - var pack = new Pack(Hoek.applyToDefaults(self._settings.pack || {}, set.pack || {})); - - // Load servers - - set.servers.forEach(function (server) { - - if (server.host && - server.host.indexOf('$env.') === 0) { - - server.host = process.env[server.host.slice(5)]; - } - - if (server.port && - typeof server.port === 'string' && - server.port.indexOf('$env.') === 0) { - - server.port = parseInt(process.env[server.port.slice(5)], 10); - } - - pack.server(server.host, server.port, server.options); - }); - - sets.push({ pack: pack, plugins: set.plugins }); - self._packs.push(pack); - }); - - // Register plugins - - Async.forEachSeries(sets, function (set, next) { - - set.pack.require(set.plugins, next); - }, - function (err) { - - return callback(err); - }); -}; - - -internals.Composer.prototype.start = function (callback) { - - callback = callback || Hoek.ignore; - - Async.forEachSeries(this._packs, function (pack, next) { - - pack.start(next); - }, - function (err) { - - Hoek.assert(!err, 'Failed starting plugins:', err && err.message); - return callback(); - }); -}; - - -internals.Composer.prototype.stop = function (options, callback) { - - if (typeof options === 'function') { - callback = options; - options = {}; - } - - callback = callback || Hoek.ignore; - - Async.forEach(this._packs, function (pack, next) { - - pack.stop(options, next); - }, - callback); -}; diff --git a/lib/defaults.js b/lib/defaults.js index 01a710cb6..e92c43763 100755 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -140,6 +140,9 @@ exports.cors = { credentials: false }; + +// Security headers + exports.security = { hsts: 15768000, xframe: 'deny', @@ -160,6 +163,12 @@ exports.cache = { // Primary cache configurati exports.state = { + // Validation settings + + strictHeader: undefined, // Defaults to server.settings.state.cookies.strictHeader + failAction: undefined, // Defaults to server.settings.state.cookies.failAction + clearInvalid: undefined, // Defaults to server.settings.state.cookies.clearInvalid + // Cookie attributes isSecure: false, diff --git a/lib/directory.js b/lib/directory.js index 545b62526..79e6e2d5c 100755 --- a/lib/directory.js +++ b/lib/directory.js @@ -140,7 +140,7 @@ exports.handler = function (route, options) { !request.server.settings.router.stripTrailingSlash && !hasTrailingSlash) { - return reply().redirect(resource + '/'); + return reply.redirect(resource + '/'); } if (!index) { diff --git a/lib/handler.js b/lib/handler.js index 8f854bbdd..3ca2a7bb0 100755 --- a/lib/handler.js +++ b/lib/handler.js @@ -14,6 +14,7 @@ var Methods = require('./methods'); var internals = {}; + exports.execute = function (request, next) { var finalize = function (err, result) { @@ -73,7 +74,7 @@ internals.handler = function (request, callback) { // Check for Error result if (response.isBoom) { - request.log(['hapi', 'handler', 'error'], { msec: timer.elapsed() }); + request.log(['hapi', 'handler', 'error'], { msec: timer.elapsed(), error: response.message }); return callback(response); } @@ -141,6 +142,11 @@ exports.replyInterface = function (request, finalize, base) { request._clearState(name); }; + reply.redirect = function (location) { + + return internals.wrap('', request, finalize).redirect(location); + } + return reply; }; diff --git a/lib/index.js b/lib/index.js index 61a8ab983..8058c36ac 100755 --- a/lib/index.js +++ b/lib/index.js @@ -4,7 +4,6 @@ exports.version = require('../package.json').version; exports.error = exports.Error = exports.boom = exports.Boom = require('boom'); exports.Server = require('./server'); exports.Pack = require('./pack'); -exports.Composer = require('./composer'); exports.state = { prepareValue: require('./state').prepareValue diff --git a/lib/methods.js b/lib/methods.js index 23ab3cab5..377ab0018 100755 --- a/lib/methods.js +++ b/lib/methods.js @@ -47,7 +47,7 @@ internals.Methods.prototype._add = function (name, fn, options, env) { options = options || {}; Schema.assert('method', options, name); - var settings = Hoek.clone(options); + var settings = Hoek.cloneWithShallow(options, ['bind']); settings.generateKey = settings.generateKey || internals.generateKey; var bind = settings.bind || (env && env.bind) || null; diff --git a/lib/pack.js b/lib/pack.js index 8e3871935..41c87410e 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -5,13 +5,14 @@ var Events = require('events'); var Async = require('async'); var Catbox = require('catbox'); var Hoek = require('hoek'); +var Kilt = require('kilt'); var Server = require('./server'); var Views = require('./views'); var Defaults = require('./defaults'); var Ext = require('./ext'); var Methods = require('./methods'); var Handler = require('./handler'); -var Hapi = require('./index'); +var Schema = require('./schema'); var Utils = require('./utils'); @@ -25,28 +26,20 @@ exports = module.exports = internals.Pack = function (options) { options = options || {}; this._settings = {}; - - if (options.requirePath) { - this._settings.requirePath = Path.resolve(options.requirePath); - } - this._settings.debug = Hoek.applyToDefaults(Defaults.server.debug, options.debug); this._servers = []; // List of all pack server members this._byLabel = {}; // Server [ids] organized by labels this._byId = {}; // Servers indexed by id - this._env = {}; // Plugin-specific environment (e.g. views manager) this._caches = {}; // Cache clients this._methods = new Methods(this); // Server methods this._handlers = {}; // Registered handlers + this._events = new Events.EventEmitter(); // Pack-only events - this.list = {}; // Loaded plugins by name this.plugins = {}; // Exposed plugin properties by name - this.events = new Events.EventEmitter(); // Consolidated subscription to all servers' events + this.events = new Kilt(this._events); // Consolidated server events this.app = options.app || {}; - this.hapi = require('./'); - if (options.cache) { var caches = Array.isArray(options.cache) ? options.cache : [options.cache]; for (var i = 0, il = caches.length; i < il; ++i) { @@ -128,87 +121,162 @@ internals.Pack.prototype._server = function (server) { // Subscribe to events - ['request', 'response', 'tail', 'internalError'].forEach(function (event) { + this.events.addEmitter(server); - server.on(event, function (request, data, tags) { + return server; +}; + + +internals.Pack.prototype._select = function (labels, subset) { - self.events.emit(event, request, data, tags); + var self = this; + + Hoek.assert(!labels || typeof labels === 'string' || Array.isArray(labels), 'Bad labels object type (undefined or array required)'); + labels = labels && [].concat(labels); + + var ids = []; + if (labels) { + labels.forEach(function (label) { + + ids = ids.concat(self._byLabel[label] || []); }); + + ids = Hoek.unique(ids); + } + else { + ids = Object.keys(this._byId); + } + + var result = { + servers: [], + index: {} + }; + + ids.forEach(function (id) { + + if (!subset || + subset[id]) { + + result.servers.push(self._byId[id]); + result.index[id] = true; + } }); - return server; + return result; }; -internals.Pack.prototype.log = function (tags, data, timestamp, _server) { +internals.Pack.prototype.register = function (plugins /*, [options], callback */) { - tags = (Array.isArray(tags) ? tags : [tags]); - var now = (timestamp ? (timestamp instanceof Date ? timestamp.getTime() : timestamp) : Date.now()); + var options = (typeof arguments[1] === 'object' ? arguments[1] : {}); + var callback = (typeof arguments[1] === 'object' ? arguments[2] : arguments[1]); - var event = { - server: (_server ? _server.info.uri : undefined), - timestamp: now, - tags: tags, - data: data + var state = { + dependencies: [] }; - var tagsMap = Hoek.mapToObject(event.tags); - - this.events.emit('log', event, tagsMap); + return this._register(plugins, options, state, function (err) { - if (_server) { - _server.emit('log', event, tagsMap); - } + if (err) { + return callback(err); + } - if (this._settings.debug && - this._settings.debug.request && - Hoek.intersect(tagsMap, this._settings.debug.request, true)) { + for (var i = 0, il = state.dependencies.length; i < il; ++i) { + var dependency = state.dependencies[i]; + for (var s = 0, sl = dependency.servers.length; s < sl; ++s) { + var server = dependency.servers[s]; + for (var d = 0, dl = dependency.deps.length; d < dl; ++d) { + var dep = dependency.deps[d]; + Hoek.assert(server._registrations[dep], 'Plugin', dependency.plugin, 'missing dependency', dep, 'in server:', server.info.uri); + } + } + } - console.error('Debug:', event.tags.join(', '), (data ? '\n ' + (data.stack || (typeof data === 'object' ? Utils.stringify(data) : data)) : '')); - } + return callback(); + }); }; -internals.Pack.prototype.register = function (plugin/*, [options], callback */) { +internals.Pack.prototype._register = function (plugins, options, state, callback) { - // Validate arguments + var self = this; + + /* + var register = function (plugin, options, next) { next(); } + register.attributes = { + name: 'plugin', + version: '1.1.1', + pkg: require('../package.json'), + multiple: false + }; + + plugin = { + register: register, // plugin: { register } when assigned a directly required module + name: 'plugin', // || register.attributes.name || register.attributes.pkg.name + version: '1.1.1', // -optional- || register.attributes.version || register.attributes.pkg.version + multiple: false, // -optional- || register.attributes.multiple + options: {} // -optional- + }; + */ + + var registrations = []; + plugins = [].concat(plugins); + for (var i = 0, il = plugins.length; i < il; ++i) { + var plugin = plugins[i]; + var hint = (plugins.length > 1 ? '(' + i + ')' : ''); + + Hoek.assert(typeof plugin === 'object', 'Invalid plugin object', hint); + Hoek.assert(!!plugin.register ^ !!plugin.plugin, 'One of plugin or register required but cannot include both', hint); + Hoek.assert(typeof plugin.register === 'function' || (typeof plugin.plugin === 'object' && typeof plugin.plugin.register === 'function'), 'Plugin register must be a function or a required plugin module', hint); + + var register = plugin.register || plugin.plugin.register; + var attributes = register.attributes || {}; + + Hoek.assert(plugin.name || attributes, 'Incompatible plugin missing register function attributes', hint); + Hoek.assert(plugin.name || attributes.name || (attributes.pkg && attributes.pkg.name), 'Missing plugin name', hint); + + var item = { + register: register, + name: plugin.name || attributes.name || attributes.pkg.name, + version: plugin.version || attributes.version || (attributes.pkg && attributes.pkg.version) || '0.0.0', + multiple: plugin.multiple || attributes.multiple || false, + options: plugin.options + }; + + registrations.push(item); + } - var options = (arguments.length === 3 ? arguments[1] : null); - var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); + Async.forEachSeries(registrations, function (item, next) { - this._register(plugin, options, callback, null); + self._plugin(item, options, state, next); + }, callback); }; -internals.Pack.prototype._register = function (plugin, options, callback, _dependencies) { +internals.Pack.prototype._plugin = function (plugin, registerOptions, state, callback) { var self = this; - // Validate arguments + // Validate options - Hoek.assert(plugin, 'Missing plugin'); - Hoek.assert(callback, 'Missing callback'); - Hoek.assert(!this._env[plugin.name], 'Plugin already registered:', plugin.name); - Hoek.assert(plugin.name, 'Plugin missing name'); - Hoek.assert(plugin.name !== '?', 'Plugin name cannot be \'?\''); - Hoek.assert(plugin.version, 'Plugin missing version'); - Hoek.assert(plugin.register && typeof plugin.register === 'function', 'Plugin missing register() method'); - - var dependencies = _dependencies || {}; + Schema.assert('register', registerOptions); // Setup environment var env = { name: plugin.name, - path: plugin.path, - bind: null + path: null, + bind: null, + route: { + prefix: registerOptions.route && registerOptions.route.prefix, + vhost: registerOptions.route && registerOptions.route.vhost + } }; - this._env[plugin.name] = env; - - // Add plugin to servers lists - - this.list[plugin.name] = plugin; + if (state.route) { + env.route.prefix = (state.route.prefix || '') + (env.route.prefix || '') || undefined; + env.route.vhost = state.route.vhost || env.route.vhost; + } // Setup pack interface @@ -219,14 +287,19 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen var methods = { length: selection.servers.length, servers: selection.servers, + events: new Kilt(selection.servers, self._events), select: function (/* labels */) { var labels = Hoek.flatten(Array.prototype.slice.call(arguments)); return step(labels, selection.index); }, - _expose: function (/* key, value */) { + expose: function (/* key, value */) { - internals.expose(selection.servers, plugin, arguments); + internals.expose(selection.servers, plugin, arguments); // server.plugins + + if (selection.servers.length === self._servers.length) { + internals.expose([self], plugin, arguments); // pack.plugins + } }, route: function (options) { @@ -249,78 +322,73 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen ext: function () { self._applySync(selection.servers, Server.prototype._ext, [arguments[0], arguments[1], arguments[2], env]); + }, + dependency: function (deps, after) { + + Hoek.assert(!after || typeof after === 'function', 'Invalid after method'); + + deps = [].concat(deps); + state.dependencies.push({ plugin: plugin.name, servers: selection.servers, deps: deps }); + + if (after) { + self._ext.add('onPreStart', after, { after: deps }, env); + } + }, + register: function (plugins /*, [options], callback */) { + + var options = (typeof arguments[1] === 'object' ? arguments[1] : {}); + var callback = (typeof arguments[1] === 'object' ? arguments[2] : arguments[1]); + + var localState = { + dependecies: state.dependecies, + route: env.route, + selection: selection.index + }; + + internals.Pack.prototype._register.call(self, plugins, options, localState, callback); } }; - methods.expose = methods._expose; - return methods; }; // Setup root pack object - var root = step(); + var root = step(registerOptions.select, state.selection); - root.version = Hapi.version; - root.hapi = self.hapi; + root.hapi = require('../'); + root.version = root.hapi.version; root.app = self.app; - root.path = plugin.path; root.plugins = self.plugins; - root.events = self.events; root.methods = self._methods.methods; - root.handlers = self._handlers; - - root.expose = function (/* key, value */) { - - root._expose.apply(null, arguments); - internals.expose([self], plugin, arguments); - }; root.log = function (tags, data, timestamp) { self.log(tags, data, timestamp); }; - root.dependency = function (deps, after) { - - Hoek.assert(!after || typeof after === 'function', 'Invalid after method'); - - dependencies[plugin.name] = dependencies[plugin.name] || []; - deps = [].concat(deps); - deps.forEach(function (dep) { - - if (!self._env[dep]) { - dependencies[plugin.name].push(dep); - } - }); - - if (after) { - self._ext.add('onPreStart', after, { after: deps }, env); - } - }; - root.after = function (method) { self._ext.add('onPreStart', method, {}, env); }; - root.loader = function (requireFunc) { - - Hoek.assert(!requireFunc || typeof requireFunc === 'function', 'Argument must be null or valid function'); - root._requireFunc = requireFunc; - }; - root.bind = function (bind) { Hoek.assert(typeof bind === 'object', 'bind must be an object'); env.bind = bind; }; + root.path = function (path) { + + Hoek.assert(path && typeof path === 'string', 'path must be a non-empty string'); + env.path = path; + } + root.views = function (options) { Hoek.assert(!env.views, 'Cannot set plugin views manager more than once'); - options.basePath = options.basePath || plugin.path; - env.views = new Views.Manager(options, root._requireFunc); + var override = (!options.basePath && env.path ? { basePath: env.path } : null); + env.views = new Views.Manager(options, override); }; root.method = function (/* name, method, options */) { @@ -345,28 +413,19 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen return self._provisionCache(options, 'plugin', plugin.name, options.segment); }; - root.require = function (name/*, [options], requireCallback*/) { - - var options = (arguments.length === 3 ? arguments[1] : null); - var requireCallback = (arguments.length === 3 ? arguments[2] : arguments[1]); - - self._require(name, options, requireCallback, root._requireFunc); - }; - env.root = root; - // Register - - plugin.register.call(null, root, options || {}, function (err) { + // Protect against multiple registrations - if (!_dependencies && - dependencies[plugin.name]) { + for (var i = 0, il = root.servers.length; i < il; ++i) { + var server = root.servers[i]; + Hoek.assert(plugin.multiple || !server._registrations[plugin.name], 'Plugin', plugin.name, 'already registered in:', server.info.uri); + server._registrations[plugin.name] = plugin; + } - Hoek.assert(!dependencies[plugin.name].length, 'Plugin', plugin.name, 'missing dependencies:', dependencies[plugin.name].join(', ')); - } + // Register - callback(err); - }); + plugin.register.call(null, root, plugin.options || {}, callback); }; @@ -388,184 +447,32 @@ internals.expose = function (dests, plugin, args) { }; -internals.Pack.prototype._select = function (labels, subset) { - - var self = this; - - Hoek.assert(!labels || Array.isArray(labels), 'Bad labels object type (undefined or array required)'); - - var ids = []; - if (labels) { - labels.forEach(function (label) { - - ids = ids.concat(self._byLabel[label] || []); - }); - - ids = Hoek.unique(ids); - } - else { - ids = Object.keys(this._byId); - } - - var result = { - servers: [], - index: {} - }; - - ids.forEach(function (id) { - - if (subset && - !subset[id]) { - - return; - } - - result.servers.push(self._byId[id]); - result.index[id] = true; - }); - - return result; -}; - - -/* - name: - 'plugin' - module in main process node_module directory - './plugin' - relative path to file where require is called - '/plugin' - absolute path - { 'plugin': { plugin-options } } - object where keys are loaded as module names (above) and values are plugin options - [ 'plugin' ] - array of plugin names, without plugin options -*/ - -internals.Pack.prototype.require = function (name/*, [options], callback*/) { - - var options = (arguments.length === 3 ? arguments[1] : null); - var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); - - this._require(name, options, callback); -}; - - -internals.Pack.prototype._require = function (name, options, callback, requireFunc) { - - var self = this; - - Hoek.assert(name && (typeof name === 'string' || typeof name === 'object'), 'Invalid plugin name(s) object: must be string, object, or array'); - Hoek.assert(!options || typeof name === 'string', 'Cannot provide options in a multi-plugin operation'); - - requireFunc = requireFunc || require; - - var callerPath = internals.getSourceFilePath(); // Must be called outside any other function to keep call stack size identical - - var parse = function () { - - var registrations = []; - - if (typeof name === 'string') { - registrations.push({ name: name, options: options }); - } - else if (Array.isArray(name)) { - name.forEach(function (item) { - - registrations.push({ name: item, options: null }); - }); - } - else { - Object.keys(name).forEach(function (item) { - - registrations.push({ name: item, options: name[item] }); - }); - } - - var dependencies = {}; - Async.forEachSeries(registrations, function (item, next) { - - load(item, dependencies, next); - }, - function (err) { - - Object.keys(dependencies).forEach(function (deps) { - - dependencies[deps].forEach(function (dep) { - - Hoek.assert(self._env[dep], 'Plugin', deps, 'missing dependencies:', dep); - }); - }); - - return callback(err); - }); - }; - - var load = function (item, dependencies, next) { - - var itemName = item.name; - if (itemName[0] === '.') { - itemName = Path.join(callerPath, itemName); - } - else if (itemName[0] !== '/' && - self._settings.requirePath) { - - itemName = Path.join(self._settings.requirePath, itemName); - } - - var packageFile = Path.join(itemName, 'package.json'); - - var mod = requireFunc(itemName); // Will throw if require fails - var pkg = requireFunc(packageFile); +internals.Pack.prototype.log = function (tags, data, timestamp, _server) { - var plugin = { - name: pkg.name, - version: pkg.version, - register: mod.register, - path: internals.packagePath(pkg.name, packageFile) - }; + tags = (Array.isArray(tags) ? tags : [tags]); + var now = (timestamp ? (timestamp instanceof Date ? timestamp.getTime() : timestamp) : Date.now()); - self._register(plugin, item.options, next, dependencies); + var event = { + server: (_server ? _server.info.uri : undefined), + timestamp: now, + tags: tags, + data: data }; - parse(); -}; - - -internals.getSourceFilePath = function () { + var tagsMap = Hoek.mapToObject(event.tags); - var stack = Hoek.callStack(); - var callerFile = ''; - var i = 0; - var il = stack.length; + this._events.emit('log', event, tagsMap); - while (callerFile === '' && i < il) { - var stackLine = stack[i]; - if (stackLine[3].lastIndexOf('.require') === stackLine[3].length - 8) { // The file that calls require is next - callerFile = stack[i + 1][0]; - } - ++i; + if (_server) { + _server.emit('log', event, tagsMap); } - return Path.dirname(callerFile); -}; - - -internals.packagePath = function (name, packageFile) { - - var path = null; - - var keys = Object.keys(require.cache); - var i = 0; - var il = keys.length; + if (this._settings.debug && + this._settings.debug.request && + Hoek.intersect(tagsMap, this._settings.debug.request, true)) { - while (path === null && i < il) { - var key = keys[i]; - if (key.indexOf(packageFile) === key.length - packageFile.length) { - var record = require.cache[key]; - if (record.exports.name === name) { - path = Path.dirname(key); - } - } - ++i; + console.error('Debug:', event.tags.join(', '), (data ? '\n ' + (data.stack || (typeof data === 'object' ? Utils.stringify(data) : data)) : '')); } - - return path; }; @@ -591,7 +498,7 @@ internals.Pack.prototype.start = function (callback) { self._caches[cache].client.start(next); }, function (err) { - self.events.emit('start'); + self._events.emit('start'); if (callback) { return callback(err); @@ -613,7 +520,7 @@ internals.Pack.prototype.stop = function (options, callback) { this._apply(this._servers, Server.prototype._stop, [options], function () { - self.events.emit('stop'); + self._events.emit('stop'); if (callback) { callback(); @@ -716,3 +623,144 @@ internals.Pack.prototype._invoke = function (event, callback) { return callback(err); }); }; + + +/* +var config1 = { + pack: { + cache: 'redis', + app: { + 'app-specific': 'value' + } + }, + servers: [ + { + port: 8001, + options: { + labels: ['api', 'nasty'] + } + }, + { + host: 'localhost', + port: '$env.PORT', + options: { + labels: ['api', 'nice'] + } + } + ], + plugins: { + furball: { + version: false, + plugins: '/' + }, + other: [ + { + select: ['b'], + options: { + version: false, + plugins: '/' + } + } + ] + } +}; +*/ + +internals.Pack.compose = function (manifest /*, [options], callback */) { + + var options = arguments.length === 2 ? {} : arguments[1]; + var callback = arguments.length === 2 ? arguments[1] : arguments[2]; + + // Create pack + + Hoek.assert(manifest.servers && manifest.servers.length, 'Manifest missing servers definition'); + Hoek.assert(manifest.plugins, 'Manifest missing plugins definition'); + Hoek.assert(options, 'Invalid options'); + Hoek.assert(typeof callback === 'function', 'Invalid callback'); + + var pack = new internals.Pack(manifest.pack); + + // Load servers + + manifest.servers.forEach(function (server) { + + if (server.host && + server.host.indexOf('$env.') === 0) { + + server.host = process.env[server.host.slice(5)]; + } + + if (server.port && + typeof server.port === 'string' && + server.port.indexOf('$env.') === 0) { + + server.port = parseInt(process.env[server.port.slice(5)], 10); + } + + pack.server(server.host, server.port, server.options); + }); + + // Load plugin + + var names = Object.keys(manifest.plugins); + Async.forEachSeries(names, function (name, nextName) { + + var item = manifest.plugins[name]; + var path = name; + if (options.relativeTo && + path[0] === '.') { + + path = Path.join(options.relativeTo, path); + } + + /* + simple: { + key: 'value' + }, + custom: [ + { + select: ['b'], + options: { + key: 'value' + } + } + ] + */ + + var plugins = []; + if (Array.isArray(item)) { + item.forEach(function (instance) { + + var registerOptions = Hoek.cloneWithShallow(instance, 'options'); + delete registerOptions.options; + + plugins.push({ + module: { + plugin: require(path), + options: instance.options + }, + apply: registerOptions + }); + }); + } + else { + plugins.push({ + module: { + plugin: require(path), + options: item + }, + apply: {} + }); + } + + Async.forEachSeries(plugins, function (plugin, nextRegister) { + + + pack.register(plugin.module, plugin.apply, nextRegister); + }, nextName); + }, + function (err) { + + return callback(err, pack); + }); +}; diff --git a/lib/response/headers.js b/lib/response/headers.js index 99f9b411a..059322be4 100755 --- a/lib/response/headers.js +++ b/lib/response/headers.js @@ -21,7 +21,7 @@ exports.apply = function (request, next) { } if (response.settings.location) { - response._header('location', internals.location(response.settings.location, request)); + response._header('location', exports.location(response.settings.location, request.server, request)); } internals.cors(response, request); @@ -39,11 +39,15 @@ exports.apply = function (request, next) { }; -internals.location = function (uri, request) { +exports.location = function (uri, server, request) { var isAbsolute = (uri.match(/^\w+\:\/\//)); - var baseUri = request.server.settings.location || (request.server.info.protocol + '://' + (request.info.host || (request.server.info.host + ':' + request.server.info.port))); - return (isAbsolute || !baseUri ? uri : baseUri + (uri.charAt(0) === '/' ? '' : '/') + uri); + if (isAbsolute) { + return uri; + } + + var baseUri = server.settings.location || (server.info.protocol + '://' + ((request && request.info.host) || (server.info.host + ':' + server.info.port))); + return baseUri + (uri.charAt(0) === '/' ? '' : '/') + uri; }; diff --git a/lib/route.js b/lib/route.js index 83fa2ff7f..929f1160d 100755 --- a/lib/route.js +++ b/lib/route.js @@ -21,27 +21,38 @@ exports = module.exports = internals.Route = function (options, server, env) { var self = this; + // Apply plugin environment (before schema validation) + + if (env && + (env.route.vhost || env.route.prefix)) { + + options = Hoek.cloneWithShallow(options, ['config']); + options.path = (env.route.prefix || '') + options.path; + options.vhost = env.route.vhost || options.vhost; + } + // Setup and validate route configuration Hoek.assert(options.handler || (options.config && options.config.handler), 'Missing or undefined handler:', options.path); Hoek.assert(!!options.handler ^ !!(options.config && options.config.handler), 'Handler must only appear once:', options.path); // XOR - Hoek.assert(options.path.match(internals.Route.pathRegex.validatePath), 'Invalid path:', options.path); + Hoek.assert(internals.Route.pathRegex.validatePath.test(options.path), 'Invalid path:', options.path); Hoek.assert(options.path.match(internals.Route.pathRegex.validatePathEncoded) === null, 'Path cannot contain encoded non-reserved path characters:', options.path); Hoek.assert(options.path === '/' || options.path[options.path.length - 1] !== '/' || !server.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when server configured to strip:', options.path); - this.settings = Hoek.clone(options.config) || {}; + this.settings = Hoek.cloneWithShallow(options.config, ['bind', 'plugins', 'app']) || {}; this.settings.handler = this.settings.handler || options.handler; Schema.assert('route', options, options.path); Schema.assert('routeConfig', this.settings, options.path); this.server = server; - this.env = env || {}; // Plugin-specific environment this.method = options.method.toLowerCase(); this.settings.method = this.method; // Expose method in settings this.settings.path = options.path; // Expose path in settings + this.settings.vhost = options.vhost; // Expose vhost in settings this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name this.settings.app = this.settings.app || {}; // Route-specific application settings + this.env = env || {}; // Plugin-specific environment // Path parsing @@ -104,7 +115,7 @@ exports = module.exports = internals.Route = function (options, server, env) { // Authentication configuration - this.settings.auth = this.server.auth._setupRoute(this.settings.auth); + this.settings.auth = this.server.auth._setupRoute(this.settings.auth, options.path); // Cache diff --git a/lib/router.js b/lib/router.js index 000cc3bb2..eddb838a8 100755 --- a/lib/router.js +++ b/lib/router.js @@ -107,8 +107,8 @@ internals.Router.prototype.add = function (configs, env) { methods.forEach(function (method) { config.method = method; - var route = new Route(config, self.server, env); // Do no use config beyond this point, use route members - var vhosts = [].concat(config.vhost || '*'); + var route = new Route(config, self.server, env); // Do no use config beyond this point, use route members + var vhosts = [].concat(route.settings.vhost || '*'); vhosts.forEach(function (vhost) { diff --git a/lib/schema.js b/lib/schema.js index 7d82afb89..7cc824dd3 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -73,6 +73,12 @@ internals.security = Joi.object({ }).allow(null, false, true); +internals.labels = Joi.alternatives([ + Joi.string(), + Joi.array().includes(Joi.string()) +]); + + internals.server = Joi.object({ app: Joi.object().allow(null), cache: Joi.alternatives(Joi.string(), internals.cache, Joi.array().includes(internals.cache)).allow(null), @@ -102,10 +108,7 @@ internals.server = Joi.object({ space: Joi.number().allow(null), suffix: Joi.string().allow(null) }), - labels: [ - Joi.string(), - Joi.array().includes(Joi.string()) - ], + labels: internals.labels, load: { maxHeapUsedBytes: Joi.number().min(0), maxEventLoopDelay: Joi.number().min(0), @@ -144,13 +147,16 @@ internals.server = Joi.object({ }); +internals.vhost = Joi.alternatives([ + Joi.string().hostname(), + Joi.array().includes(Joi.string().hostname()).min(1) +]); + + internals.route = Joi.object({ method: Joi.alternatives(Joi.string(), Joi.array().includes(Joi.string()).min(1)).required(), path: Joi.string().required(), - vhost: [ - Joi.string(), - Joi.array() - ], + vhost: internals.vhost, handler: Joi.any(), // Validated in route.config config: Joi.object().allow(null) }); @@ -168,6 +174,27 @@ internals.pre = [ ]; +internals.auth = Joi.alternatives([ + Joi.string(), + Joi.object({ + mode: Joi.string().valid('required', 'optional', 'try'), + scope: [ + Joi.string(), + Joi.array() + ], + tos: Joi.string().allow(false), + entity: Joi.string().valid('user', 'app', 'any'), + strategy: Joi.string(), + strategies: Joi.array().min(1), + payload: [ + Joi.string().valid('required', 'optional'), + Joi.boolean() + ] + }) + .without('strategy', 'strategies') +]); + + internals.routeConfig = Joi.object({ pre: Joi.array().includes(internals.pre.concat(Joi.array().includes(internals.pre).min(1))), handler: [ @@ -188,25 +215,7 @@ internals.routeConfig = Joi.object({ uploads: Joi.string(), failAction: Joi.string().valid('error', 'log', 'ignore') }), - auth: [ - Joi.object({ - mode: Joi.string().valid('required', 'optional', 'try'), - scope: [ - Joi.string(), - Joi.array() - ], - tos: Joi.string().allow(false), - entity: Joi.string(), - strategy: Joi.string(), - strategies: Joi.array(), - payload: [ - Joi.string(), - Joi.boolean() - ] - }), - Joi.boolean().allow(false), - Joi.string() - ], + auth: internals.auth.allow(false), validate: Joi.object({ headers: Joi.alternatives(Joi.object(), Joi.func()).allow(null, false, true), params: Joi.alternatives(Joi.object(), Joi.func()).allow(null, false, true), @@ -238,11 +247,11 @@ internals.routeConfig = Joi.object({ description: Joi.string(), notes: [ Joi.string(), - Joi.array() + Joi.array().includes(Joi.string()) ], tags: [ Joi.string(), - Joi.array() + Joi.array().includes(Joi.string()) ] }); @@ -300,12 +309,11 @@ internals['view handler'] = Joi.alternatives([ internals.view = internals.viewBase.keys({ - module: Joi.alternatives([ - Joi.object({ - compile: Joi.func().required() - }).options({ allowUnknown: true }), - Joi.string() - ]).required() + module: Joi.object({ + compile: Joi.func().required() + }) + .options({ allowUnknown: true }) + .required() }); @@ -327,3 +335,32 @@ internals.method = Joi.object({ generateKey: Joi.func(), cache: internals.cachePolicy }); + + +internals.register = Joi.object({ + route: Joi.object({ + prefix: Joi.string().regex(/^\/.+/), + vhost: internals.vhost + }), + select: internals.labels +}); + + +internals.state = Joi.object({ + strictHeader: Joi.boolean(), + failAction: Joi.string().valid('error', 'log', 'ignore'), + clearInvalid: Joi.boolean(), + isSecure: Joi.boolean(), + isHttpOnly: Joi.boolean(), + path: Joi.string(), + domain: Joi.string(), + ttl: Joi.number(), + encoding: Joi.string().valid('base64json', 'base64', 'form', 'iron', 'none'), + sign: Joi.object({ + password: Joi.string(), + integrity: Joi.object() + }), + iron: Joi.object(), + password: Joi.string(), + autoValue: Joi.any() +}); \ No newline at end of file diff --git a/lib/server.js b/lib/server.js index b6043257c..a6e7c2127 100755 --- a/lib/server.js +++ b/lib/server.js @@ -16,6 +16,7 @@ var Router = require('./router'); var Schema = require('./schema'); var Views = require('./views'); var Ext = require('./ext'); +var Headers = require('./response/headers'); // Pack delayed required inline @@ -69,7 +70,7 @@ exports = module.exports = internals.Server = function (/* host, port, options * args[key] = argVal; } - this.settings = Hoek.applyToDefaults(Defaults.server, args.options || {}); + this.settings = Hoek.applyToDefaultsWithShallow(Defaults.server, args.options || {}, ['app', 'plugins', 'views']); Schema.assert('server', this.settings); this.settings.labels = Hoek.unique([].concat(this.settings.labels)); // Convert string to array and removes duplicates @@ -121,6 +122,7 @@ exports = module.exports = internals.Server = function (/* host, port, options * this._ext = new Ext(['onRequest', 'onPreAuth', 'onPostAuth', 'onPreHandler', 'onPostHandler', 'onPreResponse']); this._stateDefinitions = {}; + this._registrations = {}; if (args.pack) { this.pack = args.pack; @@ -461,7 +463,9 @@ internals.Server.prototype.state = function (name, options) { Hoek.assert(name && typeof name === 'string', 'Invalid name'); Hoek.assert(!this._stateDefinitions[name], 'State already defined:', name); - Hoek.assert(!options || !options.encoding || ['base64json', 'base64', 'form', 'iron', 'none'].indexOf(options.encoding) !== -1, 'Bad encoding'); + if (options) { + Schema.assert('state', options, name); + } this._stateDefinitions[name] = Hoek.applyToDefaults(Defaults.state, options || {}); }; @@ -513,3 +517,9 @@ internals.Server.prototype.handler = function () { return this.pack._handler.apply(this.pack, arguments); }; + + +internals.Server.prototype.location = function (uri, request) { + + return Headers.location(uri, this, request); +}; diff --git a/lib/state.js b/lib/state.js index 3521a9d65..3be2bf996 100755 --- a/lib/state.js +++ b/lib/state.js @@ -39,7 +39,7 @@ internals.validateRx = { exports.parseCookies = function (request, next) { - var prepare = function () { + var parse = function () { request.state = {}; @@ -49,12 +49,10 @@ exports.parseCookies = function (request, next) { return next(); } - header(cookies); - }; - - var header = function (cookies) { + // Parse header var state = {}; + var names = []; var verify = cookies.replace(internals.parseRx, function ($0, $1, $2, $3) { var name = $1; @@ -69,6 +67,7 @@ exports.parseCookies = function (request, next) { } else { state[name] = value; + names.push(name); } return ''; @@ -82,43 +81,37 @@ exports.parseCookies = function (request, next) { return; // shouldStop calls next() } - // Validate cookie + // Parse cookies + + var parsed = {}; + Async.forEachSeries(names, function (name, nextName) { + + var value = state[name]; + var definition = request.server._stateDefinitions[name]; - if (request.server.settings.state.cookies.strictHeader) { - var names = Object.keys(state); - for (var i = 0, il = names.length; i < il; ++i) { - var name = names[i]; + // Validate cookie + + var strict = (definition && definition.strictHeader !== undefined ? definition.strictHeader + : request.server.settings.state.cookies.strictHeader); + if (strict) { if (!name.match(internals.validateRx.nameRx.strict)) { - if (shouldStop(cookies, name)) { + if (shouldStop(cookies, name, definition)) { return; // shouldStop calls next() } } var values = [].concat(state[name]); for (var v = 0, vl = values.length; v < vl; ++v) { - var value = values[v]; - if (!value.match(internals.validateRx.valueRx.strict)) { - if (shouldStop(cookies, name)) { + if (!values[v].match(internals.validateRx.valueRx.strict)) { + if (shouldStop(cookies, name, definition)) { return; // shouldStop calls next() } } } } - } - parse(state); - }; - - var parse = function (state) { - - var parsed = {}; - - var names = Object.keys(state); - Async.forEachSeries(names, function (name, nextName) { - - var value = state[name]; + // Check cookie format - var definition = request.server._stateDefinitions[name]; if (!definition || !definition.encoding) { @@ -132,7 +125,7 @@ exports.parseCookies = function (request, next) { unsign(name, value, definition, function (err, unsigned) { if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) { + if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { return; // shouldStop calls next() } @@ -142,7 +135,7 @@ exports.parseCookies = function (request, next) { decode(unsigned, definition, function (err, result) { if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) { + if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { return; // shouldStop calls next() } @@ -165,7 +158,7 @@ exports.parseCookies = function (request, next) { unsign(name, arrayValue, definition, function (err, unsigned) { if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) { + if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { return; // shouldStop calls next() } @@ -175,7 +168,7 @@ exports.parseCookies = function (request, next) { decode(unsigned, definition, function (err, result) { if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) { + if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { return; // shouldStop calls next() } @@ -288,23 +281,25 @@ exports.parseCookies = function (request, next) { return innerNext(null, result); }; - var shouldStop = function (error, name) { - - if (request.server.settings.state.cookies.clearInvalid && - name) { + var shouldStop = function (error, name, definition) { + var clearInvalid = (definition && definition.clearInvalid !== undefined ? definition.clearInvalid + : request.server.settings.state.cookies.clearInvalid); + if (clearInvalid && name) { request._clearState(name); } // failAction: 'error', 'log', 'ignore' - if (request.server.settings.state.cookies.failAction === 'log' || - request.server.settings.state.cookies.failAction === 'error') { + var failAction = (definition && definition.failAction !== undefined ? definition.failAction + : request.server.settings.state.cookies.failAction); + if (failAction === 'log' || + failAction === 'error') { request.log(['hapi', 'state', 'error'], error); } - if (request.server.settings.state.cookies.failAction === 'error') { + if (failAction === 'error') { next(Boom.badRequest('Bad cookie ' + (name ? 'value: ' + Hoek.escapeHtml(name) : 'header'))); return true; } @@ -312,7 +307,7 @@ exports.parseCookies = function (request, next) { return false; }; - prepare(); + parse(); }; diff --git a/lib/views.js b/lib/views.js index ff5857e44..5272ee755 100755 --- a/lib/views.js +++ b/lib/views.js @@ -7,8 +7,7 @@ var Hoek = require('hoek'); var Defaults = require('./defaults'); var Schema = require('./schema'); var Response = require('./response'); -var Schema = require('./schema'); -// Additional engine modules required in constructor +// Additional helper modules required in constructor // Declare internals @@ -18,57 +17,57 @@ var internals = {}; // View Manager -exports.Manager = internals.Manager = function (options, requireFunc) { +exports.Manager = internals.Manager = function (options, _override) { var self = this; - requireFunc = requireFunc || require; + // Save non-defaults values - var extensions = Object.keys(options.engines); - Hoek.assert(extensions.length, 'Views manager requires at least one registered extension handler'); + var engines = options.engines; + var defaultExtension = options.defaultExtension; + + // Clone options + + var defaults = Hoek.applyToDefaultsWithShallow(Defaults.views, options, ['engines']); + if (_override) { + Hoek.merge(defaults, _override); // _override cannot contain non-clonable objects + } - var defaults = Hoek.applyToDefaults(Defaults.views, options); delete defaults.engines; delete defaults.defaultExtension; + // Prepare manager state + + var extensions = Object.keys(engines); + Hoek.assert(extensions.length, 'Views manager requires at least one registered extension handler'); + this._engines = {}; - this._defaultExtension = options.defaultExtension || (extensions.length === 1 ? extensions[0] : ''); + this._defaultExtension = defaultExtension || (extensions.length === 1 ? extensions[0] : ''); // Load engines extensions.forEach(function (extension) { - var config = options.engines[extension]; - if (typeof config === 'string') { - config = { module: config }; - } + var config = engines[extension]; + var engine = {}; - // Prevent module from being cloned + if (config.compile && + typeof config.compile === 'function') { - var module = null; - if (typeof config.module === 'object') { - module = config.module; - config.module = null; + engine.module = config; + engine.config = defaults; } + else { + Schema.assert('view', config); - config = Hoek.applyToDefaults(defaults, config); - - if (module) { - config.module = module; + engine.module = config.module; + engine.config = Hoek.applyToDefaultsWithShallow(defaults, config, ['module']); } - Schema.assert('view', config); - - var engine = { - module: (typeof config.module === 'string' ? requireFunc(config.module) : config.module), - config: config, - suffix: '.' + extension - }; - - Hoek.assert(engine.module.compile, 'Invalid view engine module: missing compile()'); - + engine.suffix = '.' + extension; engine.compileFunc = engine.module.compile; - if (config.compileMode === 'sync') { + + if (engine.config.compileMode === 'sync') { engine.compileFunc = function (str, opt, next) { var compiled = null; @@ -96,7 +95,7 @@ exports.Manager = internals.Manager = function (options, requireFunc) { }; } - if (config.isCached) { + if (engine.config.isCached) { engine.cache = {}; } @@ -265,17 +264,16 @@ internals.Manager.prototype.render = function (filename, context, options, callb return callback(err); } - var layoutContext = Hoek.clone(context); - compiled(context, settings.runtimeOptions, function (err, rendered) { if (err) { return callback(Boom.badImplementation(err.message, err)); } - layoutContext[settings.layoutKeyword] = rendered; + context[settings.layoutKeyword] = rendered; + layout(context, settings.runtimeOptions, function (err, rendered) { - layout(layoutContext, settings.runtimeOptions, function (err, rendered) { + delete context[settings.layoutKeyword]; if (err) { return callback(Boom.badImplementation(err.message, err)); diff --git a/package.json b/package.json index 0a3434f88..4683b9c39 100755 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "hapi", "description": "HTTP Server framework", "homepage": "http://hapijs.com", - "version": "5.1.0", + "version": "6.0.0", "repository": { "type": "git", "url": "git://github.com/spumko/hapi" @@ -18,7 +18,7 @@ "node": ">=0.10.22" }, "dependencies": { - "hoek": "2.x.x", + "hoek": "^2.3.x", "boom": "^2.4.x", "joi": "^4.4.x", "catbox": "^2.1.x", @@ -28,6 +28,7 @@ "cryptiles": "2.x.x", "iron": "2.x.x", "topo": "1.x.x", + "kilt": "^1.1.x", "async": "0.8.x", "multiparty": "3.2.x", "mime": "1.2.x", @@ -41,7 +42,6 @@ "lab": "3.x.x", "handlebars": "1.2.x", "jade": "1.0.x", - "hapi-plugin-test": "2.x.x", "form-data": "0.1.x" }, "bin": { diff --git a/test/auth.js b/test/auth.js index aa235c652..681f0a59e 100755 --- a/test/auth.js +++ b/test/auth.js @@ -41,6 +41,71 @@ describe('Auth', function () { }); }); + it('sets default', function (done) { + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: {} } }); + server.auth.default('default'); + server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.auth.credentials.user); } }); + + server.inject('/', function (res) { + + expect(res.statusCode).to.equal(401); + + server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) { + + expect(res.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('sets default with object', function (done) { + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: {} } }); + server.auth.default({ strategy: 'default' }); + server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.auth.credentials.user); } }); + + server.inject('/', function (res) { + + expect(res.statusCode).to.equal(401); + + server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) { + + expect(res.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('throws when setting default twice', function (done) { + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: {} } }); + expect(function () { + + server.auth.default('default'); + server.auth.default('default'); + }).to.throw('Cannot set default strategy more than once'); + done(); + }); + + it('throws when setting default without strategy', function (done) { + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: {} } }); + expect(function () { + + server.auth.default({ mode: 'required' }); + }).to.throw('Default authentication strategy missing strategy name'); + done(); + }); + it('authenticates using multiple strategies', function (done) { var server = new Hapi.Server(); @@ -135,44 +200,6 @@ describe('Auth', function () { }); }); - it('enables individual route authentications', function (done) { - - var server = new Hapi.Server(); - server.auth.scheme('custom', internals.implementation); - server.auth.strategy('default', 'custom', { users: { steve: {} } }); - server.route({ - method: 'GET', - path: '/1', - config: { - handler: function (request, reply) { reply(request.auth.credentials.user); }, - auth: true - } - }); - server.route({ - method: 'GET', - path: '/2', - config: { - handler: function (request, reply) { reply('ok'); } - } - }); - - server.inject('/1', function (res) { - - expect(res.statusCode).to.equal(401); - - server.inject({ url: '/1', headers: { authorization: 'Custom steve' } }, function (res) { - - expect(res.statusCode).to.equal(200); - - server.inject('/2', function (res) { - - expect(res.statusCode).to.equal(200); - done(); - }); - }); - }); - }); - it('tries to authenticate a request', function (done) { var server = new Hapi.Server(); @@ -577,6 +604,78 @@ describe('Auth', function () { }); }); + it('throws when strategy does not support payload authentication', function (done) { + + var server = new Hapi.Server(); + var implementation = function () { return { authenticate: internals.implementation.authenticate } }; + + server.auth.scheme('custom', implementation); + server.auth.strategy('default', 'custom', true, {}); + expect(function () { + + server.route({ + method: 'POST', + path: '/', + config: { + handler: function (request, reply) { reply(request.auth.credentials.user); }, + auth: { + payload: 'required' + } + } + }); + }).to.throw('Payload validation can only be required when all strategies support it in path: /'); + done(); + }); + + it('throws when no strategy supports optional payload authentication', function (done) { + + var server = new Hapi.Server(); + var implementation = function () { return { authenticate: internals.implementation.authenticate } }; + + server.auth.scheme('custom', implementation); + server.auth.strategy('default', 'custom', true, {}); + expect(function () { + + server.route({ + method: 'POST', + path: '/', + config: { + handler: function (request, reply) { reply(request.auth.credentials.user); }, + auth: { + payload: 'optional' + } + } + }); + }).to.throw('Payload authentication requires at least one strategy with payload support in path: /'); + done(); + }); + + it('allows one strategy to supports optional payload authentication while another does not', function (done) { + + var server = new Hapi.Server(); + var implementation = function () { return { authenticate: internals.implementation.authenticate } }; + + server.auth.scheme('custom1', implementation); + server.auth.scheme('custom2', internals.implementation); + server.auth.strategy('default1', 'custom1', {}); + server.auth.strategy('default2', 'custom2', {}); + expect(function () { + + server.route({ + method: 'POST', + path: '/', + config: { + handler: function (request, reply) { reply(request.auth.credentials.user); }, + auth: { + strategies: ['default2', 'default1'], + payload: 'optional' + } + } + }); + }).to.not.throw(); + done(); + }); + it('skips request payload by default', function (done) { var server = new Hapi.Server(); @@ -601,7 +700,7 @@ describe('Auth', function () { var server = new Hapi.Server(); server.auth.scheme('custom', internals.implementation); - server.auth.strategy('default', 'custom', { users: { skip: { } } }); + server.auth.strategy('default', 'custom', true, { users: { skip: { } } }); server.route({ method: 'POST', path: '/', @@ -765,7 +864,7 @@ describe('Auth', function () { var server = new Hapi.Server(); server.auth.scheme('custom', internals.implementation); - server.auth.strategy('default', 'custom', { users: { steve: {} } }); + server.auth.strategy('default', 'custom', true, { users: { steve: {} } }); var handler = function (request, reply) { @@ -786,6 +885,40 @@ describe('Auth', function () { }); }); }); + + it('tests a request', function (done) { + + var handler = function (request, reply) { + + request.server.auth.test('default', request, function (err, credentials) { + + if (err) { + return reply({ status: false }); + } + + return reply({ status: true, user: credentials.name }); + }); + }; + + var server = new Hapi.Server(); + server.auth.scheme('custom', internals.implementation); + server.auth.strategy('default', 'custom', { users: { steve: { name: 'steve' } } }); + server.route({ method: 'GET', path: '/', handler: handler }); + + server.inject('/', function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result.status).to.equal(false); + + server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result.status).to.equal(true); + expect(res.result.user).to.equal('steve'); + done(); + }); + }); + }); }); diff --git a/test/cli.js b/test/cli.js index 8a8282b4a..3d74bda2f 100755 --- a/test/cli.js +++ b/test/cli.js @@ -60,7 +60,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -114,7 +114,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -172,7 +172,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -200,7 +200,7 @@ describe('Hapi command line', function () { }); }); - it('errors when it can\'t require the extra module', function (done) { + it('errors when it cannot require the extra module', function (done) { var manifest = { pack: { @@ -227,7 +227,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -256,7 +256,7 @@ describe('Hapi command line', function () { done(); }); }); - it('errors when it can\'t require the extra module from absolute path', function (done) { + it('errors when it cannot require the extra module from absolute path', function (done) { var manifest = { pack: { @@ -283,7 +283,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; @@ -340,7 +340,7 @@ describe('Hapi command line', function () { } ], plugins: { - '--loaded': {} + './--loaded': {} } }; diff --git a/test/composer.js b/test/composer.js deleted file mode 100755 index 38dccc753..000000000 --- a/test/composer.js +++ /dev/null @@ -1,237 +0,0 @@ -// Load modules - -var Lab = require('lab'); -var Hapi = require('..'); - - -// Declare internals - -var internals = {}; - - -// Test shortcuts - -var expect = Lab.expect; -var before = Lab.before; -var after = Lab.after; -var describe = Lab.experiment; -var it = Lab.test; - - -describe('Composer', function () { - - it('composes pack', function (done) { - - var manifest = { - pack: { - cache: { - engine: 'catbox-memory' - }, - app: { - my: 'special-value' - } - }, - servers: [ - { - port: 0, - options: { - labels: ['api', 'nasty', 'test'] - } - }, - { - host: 'localhost', - port: 0, - options: { - labels: ['api', 'nice'] - } - } - ], - plugins: { - '../test/pack/--test1': [{ ext: true }, {}] - } - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - composer.start(function (err) { - - expect(err).to.not.exist; - composer.stop(function () { - - composer._packs[0]._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { - - expect(res.result).to.equal('testing123special-value'); - done(); - }); - }); - }); - }); - }); - - it('composes pack (env)', function (done) { - - var manifest = { - pack: { - cache: { - engine: 'catbox-memory' - } - }, - servers: [ - { - port: '$env.hapi_port', - options: { - labels: ['api', 'nasty', 'test'] - } - }, - { - host: '$env.hapi_host', - port: 0, - options: { - labels: ['api', 'nice'] - } - } - ], - plugins: { - '../test/pack/--test1': {} - } - }; - - process.env.hapi_port = '0'; - process.env.hapi_host = 'localhost'; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - composer.start(function (err) { - - expect(err).to.not.exist; - composer.stop(); - - composer._packs[0]._servers[0].inject({ method: 'GET', url: '/test1' }, function (res) { - - expect(res.result).to.equal('testing123'); - done(); - }); - }); - }); - }); - - it('composes pack with ports', function (done) { - - var manifest = { - servers: [ - { - port: 8000 - }, - { - port: '8001', - } - ], - plugins: {} - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - done(); - }); - }); - - it('throws when missing servers', function (done) { - - var manifest = { - plugins: {} - }; - - var composer = new Hapi.Composer(manifest); - expect(function () { - - composer.compose(function (err) { }); - }).to.throw('Pack missing servers definition'); - done(); - }); - - it('composes pack with default pack settings', function (done) { - - var composer = new Hapi.Composer({ servers: [{}], plugins: {} }, { pack: { app: 'only here' } }); - composer.compose(function (err) { - - expect(err).to.not.exist; - - expect(composer._packs[0].app).to.equal('only here'); - done(); - }); - }); - - it('allows start without callback', function (done) { - - var manifest = { - servers: [ - { - port: 0, - } - ], - plugins: {} - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - composer.start(); - done(); - }); - }); - - it('allows stop without callback', function (done) { - - var manifest = { - servers: [ - { - port: 0, - } - ], - plugins: {} - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - composer.start(function () { - - composer.stop(); - done(); - }); - }); - }); - - it('throws error when start fails', function (done) { - - var manifest = { - servers: [ - { - port: 0, - } - ], - plugins: { - '../test/pack/--afterErr': {} - } - }; - - var composer = new Hapi.Composer(manifest); - composer.compose(function (err) { - - expect(err).to.not.exist; - expect(function () { - - composer.start(); - }).to.throw('Failed starting plugins: Not in the mood'); - done(); - }); - }); -}); diff --git a/test/ext.js b/test/ext.js index 800794d6c..c1dd889a6 100755 --- a/test/ext.js +++ b/test/ext.js @@ -71,7 +71,7 @@ describe('Ext', function () { var server = new Hapi.Server({ views: { - engines: { 'html': 'handlebars' }, + engines: { 'html': require('handlebars') }, path: __dirname + '/templates/valid' } }); diff --git a/test/methods.js b/test/methods.js index f8ef5978c..dd89b8774 100755 --- a/test/methods.js +++ b/test/methods.js @@ -597,4 +597,31 @@ describe('Method', function () { }); }); }); + + it('shallow copies bind config', function (done) { + + var bind = { gen: 7 }; + var method = function (id, next) { + + return next(null, { id: id, gen: this.gen++, bound: (this === bind) }); + }; + + var server = new Hapi.Server(0); + server.method('test', method, { bind: bind, cache: { expiresIn: 1000 } }); + + server.start(function () { + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(7); + expect(result.bound).to.equal(true); + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(7); + done(); + }); + }); + }); + }); }); diff --git a/test/pack.js b/test/pack.js index 659bcd170..98c354993 100755 --- a/test/pack.js +++ b/test/pack.js @@ -46,7 +46,6 @@ describe('Pack', function () { var plugin = { name: 'test', - version: '5.0.0', register: function (plugin, options, next) { var a = plugin.select('a'); @@ -95,67 +94,82 @@ describe('Pack', function () { expect(routesList(pack._servers[1])).to.deep.equal(['/a', '/all', '/sodd']); expect(routesList(pack._servers[2])).to.deep.equal(['/a', '/ab', '/all']); expect(routesList(pack._servers[3])).to.deep.equal(['/all', '/sodd', '/memoryx']); - - expect(pack._servers[0].pack.list.test.version).to.equal('5.0.0'); - done(); }); }); - it('registers plugins with options', function (done) { + it('registers plugin with options', function (done) { var pack = new Hapi.Pack(); pack.server({ labels: ['a', 'b'] }); var plugin = { name: 'test', - version: '5.0.0', register: function (plugin, options, next) { expect(options.something).to.be.true; next(); - } + }, + options: { something: true } }; - pack.register(plugin, { something: true }, function (err) { + pack.register(plugin, function (err) { expect(err).to.not.exist; done(); }); }); - it('registers plugin via server plugin interface', function (done) { + it('registers plugin via server pack interface', function (done) { var plugin = { name: 'test', - version: '2.0.0', register: function (plugin, options, next) { expect(options.something).to.be.true; next(); - } + }, + options: { something: true } }; var server = new Hapi.Server(); - server.pack.register(plugin, { something: true }, function (err) { + server.pack.register(plugin, function (err) { expect(err).to.not.exist; done(); }); }); - it('throws when plugin missing register', function (done) { + it('returns plugin error', function (done) { var plugin = { name: 'test', - version: '2.0.0' + register: function (plugin, options, next) { + + next(new Error('from plugin')); + } + }; + + var server = new Hapi.Server(); + server.pack.register(plugin, function (err) { + + expect(err).to.exist; + expect(err.message).to.equal('from plugin'); + done(); + }); + }); + + it('throws when plugin missing register', function (done) { + + var plugin = { + name: 'test' }; var server = new Hapi.Server(); expect(function () { server.pack.register(plugin, { something: true }, function (err) { }); - }).to.throw('Plugin missing register() method'); + }).to.throw('One of plugin or register required but cannot include both'); done(); }); @@ -169,31 +183,108 @@ describe('Pack', function () { done(); }); - it('requires plugin', function (done) { + it('throws when plugin contains non object plugin property', function (done) { - var pack = new Hapi.Pack(); - pack.server({ labels: ['s1', 'a', 'b'] }); - pack.server({ labels: ['s2', 'a', 'test'] }); - pack.server({ labels: ['s3', 'a', 'b', 'd', 'cache'] }); - pack.server({ labels: ['s4', 'b', 'test', 'cache'] }); + var plugin = { + name: 'test', + plugin: 5 + }; + + var server = new Hapi.Server(); + expect(function () { - pack.require('./pack/--test1', {}, function (err) { + server.pack.register(plugin, function (err) { }); + }).to.throw('Plugin register must be a function or a required plugin module'); + done(); + }); - expect(err).to.not.exist; + it('throws when plugin contains an object plugin property with invalid register', function (done) { - expect(pack._servers[0]._router.routes['get']).to.not.exist; - expect(routesList(pack._servers[1])).to.deep.equal(['/test1']); - expect(pack._servers[2]._router.routes['get']).to.not.exist; - expect(routesList(pack._servers[3])).to.deep.equal(['/test1']); + var plugin = { + name: 'test', + plugin: { + register: 5 + } + }; - expect(pack._servers[0].plugins['--test1'].add(1, 3)).to.equal(4); - expect(pack._servers[0].plugins['--test1'].glue('1', '3')).to.equal('13'); + var server = new Hapi.Server(); + expect(function () { - done(); + server.pack.register(plugin, function (err) { }); + }).to.throw('Plugin register must be a function or a required plugin module'); + done(); + }); + + it('throws when plugin contains a pkg attribute without a name', function (done) { + + var plugin = { + register: function () { } + }; + + plugin.register.attributes = { + pkg: { + + } + }; + + var server = new Hapi.Server(); + expect(function () { + + server.pack.register(plugin, function (err) { }); + }).to.throw('Missing plugin name'); + done(); + }); + + it('sets version to 0.0.0 if missing', function (done) { + + var plugin = { + register: function (plugin, options, next) { + + plugin.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(plugin.version); } }); + next(); + } + }; + + plugin.register.attributes = { + pkg: { + name: '--steve' + } + }; + + var server = new Hapi.Server(); + + server.pack.register(plugin, function (err) { + + expect(err).to.not.exist; + expect(server._registrations['--steve'].version).to.equal('0.0.0'); + server.inject('/', function (res) { + + expect(res.result).to.equal(Hapi.version); + done(); + }); }); }); - it('requires plugin with absolute path', function (done) { + it('throws when plugin sets undefined path', function (done) { + + var plugin = { + name: '--steve', + register: function (plugin, options, next) { + + plugin.path(); + next(); + } + }; + + var server = new Hapi.Server(); + expect(function () { + + server.pack.register(plugin, function (err) { }); + }).to.throw('path must be a non-empty string'); + done(); + }); + + it('registers plugin with exposed api', function (done) { var pack = new Hapi.Pack(); pack.server({ labels: ['s1', 'a', 'b'] }); @@ -201,7 +292,7 @@ describe('Pack', function () { pack.server({ labels: ['s3', 'a', 'b', 'd', 'cache'] }); pack.server({ labels: ['s4', 'b', 'test', 'cache'] }); - pack.require(__dirname + '/pack/--test1', {}, function (err) { + pack.register(require('./pack/--test1'), function (err) { expect(err).to.not.exist; @@ -217,46 +308,82 @@ describe('Pack', function () { }); }); - it('requires a plugin with options', function (done) { + it('prevents plugin from multiple registrations', function (done) { - var pack = new Hapi.Pack(); - pack.server({ labels: ['a', 'b'] }); + var plugin = { + name: 'test', + register: function (plugin, options, next) { - pack.require('./pack/--test1', { something: true }, function (err) { + plugin.route({ method: 'GET', path: '/a', handler: function (request, reply) { reply('a'); } }); + next(); + } + }; + + var server = new Hapi.Server(); + server.pack.register(plugin, function (err) { expect(err).to.not.exist; + expect(function () { + + server.pack.register(plugin, function (err) { }); + }).to.throw('Plugin test already registered in: ' + server.info.uri); + done(); }); }); - it('requires plugin via server plugin interface', function (done) { + it('allows plugin multiple registrations (attributes)', function (done) { var plugin = { name: 'test', - version: '2.0.0', register: function (plugin, options, next) { - plugin.route({ method: 'GET', path: '/a', handler: function (request, reply) { reply('a'); } }); + plugin.app.x = plugin.app.x ? plugin.app.x + 1 : 1; next(); } }; + plugin.register.attributes = { multiple: true }; + var server = new Hapi.Server(); server.pack.register(plugin, function (err) { expect(err).to.not.exist; - expect(routesList(server)).to.deep.equal(['/a']); + server.pack.register(plugin, function (err) { - expect(function () { + expect(err).to.not.exist; + expect(server.pack.app.x).to.equal(2); + done(); + }); + }); + }); - server.pack.register(plugin, function (err) { }); - }).to.throw(); + it('allows plugin multiple registrations (property)', function (done) { - done(); + var plugin = { + name: 'test', + multiple: true, + register: function (plugin, options, next) { + + plugin.app.x = plugin.app.x ? plugin.app.x + 1 : 1; + next(); + } + }; + + var server = new Hapi.Server(); + server.pack.register(plugin, function (err) { + + expect(err).to.not.exist; + server.pack.register(plugin, function (err) { + + expect(err).to.not.exist; + expect(server.pack.app.x).to.equal(2); + done(); + }); }); }); - it('requires multiple plugins using array', function (done) { + it('registers multiple plugins', function (done) { var server = new Hapi.Server({ labels: 'test' }); var log = null; @@ -265,56 +392,50 @@ describe('Pack', function () { log = [event, tags]; }); - server.pack.require(['./pack/--test1', './pack/--test2'], function (err) { + server.pack.register([require('./pack/--test1'), require('./pack/--test2')], function (err) { expect(err).to.not.exist; - expect(routesList(server)).to.deep.equal(['/test1', '/test2', '/test2/path']); + expect(routesList(server)).to.deep.equal(['/test1', '/test2']); expect(log[1].test).to.equal(true); expect(log[0].data).to.equal('abc'); done(); }); }); - it('requires multiple plugins using object', function (done) { + it('registers multiple plugins (verbose)', function (done) { var server = new Hapi.Server({ labels: 'test' }); - server.pack.require({ './pack/--test1': {}, './pack/--test2': {} }, function (err) { + var log = null; + server.pack.events.once('log', function (event, tags) { - expect(err).to.not.exist; - expect(routesList(server)).to.deep.equal(['/test1', '/test2', '/test2/path']); - done(); + log = [event, tags]; }); - }); - - it('exposes the plugin path', function (done) { - var server = new Hapi.Server({ labels: 'test' }); - server.pack.require('./pack/--test2', function (err) { + server.pack.register([{ plugin: require('./pack/--test1') }, { plugin: require('./pack/--test2') }], function (err) { expect(err).to.not.exist; - server.inject('/test2/path', function (res) { - - expect(res.result).to.equal(Path.join(process.cwd(), 'test', 'pack', '--test2')); - done(); - }); + expect(routesList(server)).to.deep.equal(['/test1', '/test2']); + expect(log[1].test).to.equal(true); + expect(log[0].data).to.equal('abc'); + done(); }); }); it('requires plugin with views', function (done) { var server = new Hapi.Server(); - server.pack.require({ './pack/--views': { message: 'viewing it' } }, function (err) { + server.pack.register({ plugin: require('./pack/--views'), options: { message: 'viewing it' } }, function (err) { expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/view' }, function (res) { + server.inject('/view', function (res) { expect(res.result).to.equal('