Example #1
0
File: index.js Project: Qey/bell
internals.implementation = function (server, options) {

    var settings = Hoek.cloneWithShallow(options, 'provider');      // Options can be reused

    // Lookup provider

    if (typeof settings.provider === 'object') {
        settings.name = 'custom';
    }
    else {
        settings.name = settings.provider;
        settings.provider = Providers[settings.provider].call(null, settings.config)
    }

    Joi.assert(settings, internals.schema);

    // Setup cookie for managing temporary authorization state

    var cookieOptions = {
        encoding: 'iron',
        path: '/',
        password: settings.password,
        isSecure: settings.isSecure !== false,                  // Defaults to true
        isHttpOnly: settings.isHttpOnly !== false,              // Defaults to true
        ttl: settings.ttl,
        domain: settings.domain,
        failAction: 'log',
        clearInvalid: true
    };

    settings.cookie = settings.cookie || 'bell-' + settings.name;
    server.state(settings.cookie, cookieOptions);

    return { authenticate: (settings.provider.protocol === 'oauth' ? OAuth.v1 : OAuth.v2)(settings) };
};
Example #2
0
internals.implementation = function (server, options) {

    let settings = Hoek.cloneWithShallow(options, 'provider');      // Options can be reused

    // Lookup provider

    if (typeof settings.provider === 'object') {
        settings.name = settings.provider.name || 'custom';
    }
    else {
        settings.name = settings.provider;
        settings.provider = Providers[settings.provider].call(null, settings.config);
    }

    const results = Joi.validate(settings, internals.schema);
    Hoek.assert(!results.error, results.error);

    // Passed validation, use Joi converted settings
    settings = results.value;

    // Setup cookie for managing temporary authorization state

    const cookieOptions = {
        encoding: 'iron',
        path: '/',
        password: settings.password,
        isSecure: settings.isSecure !== false,                  // Defaults to true
        isHttpOnly: settings.isHttpOnly !== false,              // Defaults to true
        isSameSite: false,
        ttl: settings.ttl,
        domain: settings.domain,
        ignoreErrors: true,
        clearInvalid: true
    };

    settings.cookie = settings.cookie || 'bell-' + settings.name;
    try {
        server.state(settings.cookie, cookieOptions);
    }
    catch (exception) {
        /* $lab:coverage:off$ */
        // This is to support Hapi 13.5.0 so that adding isSameSite: false option is not a breaking change
        if (exception.message.indexOf('isSameSite') === -1) {
            throw exception;
        }
        delete cookieOptions.isSameSite;
        server.state(settings.cookie, cookieOptions);
        /* $lab:coverage:on$ */
    }

    if (internals.simulate) {
        return internals.simulated(settings);
    }

    return { authenticate: (settings.provider.protocol === 'oauth' ? OAuth.v1 : OAuth.v2)(settings) };
};
Example #3
0
    _add(name, method, options, realm) {

        Hoek.assert(typeof method === 'function', 'method must be a function');
        Hoek.assert(typeof name === 'string', 'name must be a string');
        Hoek.assert(name.match(internals.methodNameRx), 'Invalid name:', name);
        Hoek.assert(!Hoek.reach(this.methods, name, { functions: false }), 'Server method function name already exists:', name);

        options = Config.apply('method', options, name);

        const settings = Hoek.cloneWithShallow(options, ['bind']);
        settings.generateKey = settings.generateKey || internals.generateKey;

        const bind = settings.bind || realm.settings.bind || null;
        const bound = !bind ? method : (...args) => method.apply(bind, args);

        // Not cached

        if (!settings.cache) {
            return this._assign(name, bound);
        }

        // Cached

        Hoek.assert(!settings.cache.generateFunc, 'Cannot set generateFunc with method caching:', name);
        Hoek.assert(settings.cache.generateTimeout !== undefined, 'Method caching requires a timeout value in generateTimeout:', name);

        settings.cache.generateFunc = (id, flags) => bound(...id.args, flags);
        const cache = this.core._cachePolicy(settings.cache, '#' + name);

        const func = function (...args) {

            const key = settings.generateKey.apply(bind, args);
            if (typeof key !== 'string') {
                return Promise.reject(Boom.badImplementation('Invalid method key when invoking: ' + name, { name, args }));
            }

            return cache.get({ id: key, args });
        };

        func.cache = {
            drop: function (...args) {

                const key = settings.generateKey.apply(bind, args);
                if (typeof key !== 'string') {
                    return Promise.reject(Boom.badImplementation('Invalid method key when invoking: ' + name, { name, args }));
                }

                return cache.drop(key);
            },
            stats: cache.stats
        };

        this._assign(name, func, func);
    }
Example #4
0
File: pack.js Project: JFickel/hapi
    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);
        }

        var serverOptions = server.options;
        if (serverOptions &&
            serverOptions.views) {

            serverOptions = Hoek.cloneWithShallow(serverOptions, 'views');
            serverOptions.views = Hoek.cloneWithShallow(serverOptions.views, 'engines');
            var engines = Object.keys(serverOptions.views.engines);
            engines.forEach(function (engine) {

                var value = serverOptions.views.engines[engine];
                if (typeof value === 'string') {
                    if (options.relativeTo &&
                        value[0] === '.') {

                        value = Path.join(options.relativeTo, value);
                    }

                    serverOptions.views.engines[engine] = require(value);
                }
            });
        }

        pack.server(server.host, server.port, serverOptions);
    });
Example #5
0
File: pack.js Project: JFickel/hapi
            item.forEach(function (instance) {

                var registerOptions = Hoek.cloneWithShallow(instance, 'options');
                delete registerOptions.options;

                plugins.push({
                    module: {
                        plugin: require(path),
                        options: instance.options
                    },
                    apply: registerOptions
                });
            });
Example #6
0
exports.createServer = function () {

    var args = Pack._args(arguments);

    var settings = Hoek.cloneWithShallow(args.options || {}, ['app', 'plugins']);
    var options = {
        cache: settings.cache,
        debug: settings.debug
    };

    delete settings.cache;

    var pack = new Pack(options);
    return pack.connection(args.host, args.port, settings);
};
Example #7
0
internals.parsePlugin = function (plugin, relativeTo) {

    plugin = Hoek.cloneWithShallow(plugin, ['options']);
    if (typeof plugin === 'string') {
        plugin = { register: plugin };
    }

    let path = plugin.register;
    if (relativeTo && path[0] === '.') {
        path = Path.join(relativeTo, path);
    }

    plugin.register = require(path);
    return plugin;
};
Example #8
0
        plugin.forEach(function (instance) {

            Hoek.assert(typeof instance === 'object', 'Invalid plugin configuration');

            var registerOptions = Hoek.cloneWithShallow(instance, 'options');
            delete registerOptions.options;

            plugins.push({
                module: {
                    register: require(path),
                    options: instance.options
                },
                apply: registerOptions
            });
        });
Example #9
0
    const onResponse = (res) => {

        // Pass-through response

        const statusCode = res.statusCode;

        if (redirects === false ||
            [301, 302, 307, 308].indexOf(statusCode) === -1) {

            return finishOnce(null, res);
        }

        // Redirection

        const redirectMethod = (statusCode === 301 || statusCode === 302 ? 'GET' : uri.method);
        let location = res.headers.location;

        res.destroy();

        if (redirects === 0) {
            return finishOnce(Boom.badGateway('Maximum redirections reached', _trace));
        }

        if (!location) {
            return finishOnce(Boom.badGateway('Received redirection without location', _trace));
        }

        if (!/^https?:/i.test(location)) {
            location = Url.resolve(uri.href, location);
        }

        const redirectOptions = Hoek.cloneWithShallow(options, internals.shallowOptions);

        redirectOptions.payload = shadow || options.payload;         // shadow must be ready at this point if set
        redirectOptions.redirects = --redirects;

        if (options.beforeRedirect) {
            options.beforeRedirect(redirectMethod, statusCode, location, redirectOptions);
        }

        const redirectReq = this.request(redirectMethod, location, redirectOptions, finishOnce, _trace);

        if (options.redirected) {
            options.redirected(statusCode, location, redirectReq);
        }
    };
Example #10
0
    const onResponse = (res) => {

        // Pass-through response

        const statusCode = res.statusCode;
        const redirectMethod = internals.redirectMethod(statusCode, uri.method, options);

        if (redirects === false ||
            !redirectMethod) {

            return finishOnce(null, res);
        }

        // Redirection

        res.destroy();

        if (redirects === 0) {
            return finishOnce(Boom.badGateway('Maximum redirections reached', _trace));
        }

        let location = res.headers.location;
        if (!location) {
            return finishOnce(Boom.badGateway('Received redirection without location', _trace));
        }

        if (!/^https?:/i.test(location)) {
            location = Url.resolve(uri.href, location);
        }

        const redirectOptions = Hoek.cloneWithShallow(options, internals.shallowOptions);
        redirectOptions.payload = shadow || options.payload;                                    // shadow must be ready at this point if set
        redirectOptions.redirects = --redirects;

        return options.beforeRedirect(redirectMethod, statusCode, location, res.headers, redirectOptions, () => {

            const redirectReq = this._request(redirectMethod, location, redirectOptions, { callback: finishOnce }, _trace);

            if (options.redirected) {
                options.redirected(statusCode, location, redirectReq);
            }
        });
    };
Example #11
0
internals.implementation = function (server, options) {

    let settings = Hoek.cloneWithShallow(options, 'provider');      // Options can be reused

    // Lookup provider

    if (typeof settings.provider === 'object') {
        settings.name = settings.provider.name || 'custom';
    }
    else {
        settings.name = settings.provider;
        settings.provider = Providers[settings.provider].call(null, settings.config);
    }

    const results = Joi.validate(settings, internals.schema);
    Hoek.assert(!results.error, results.error);

    // Passed validation, use Joi converted settings
    settings = results.value;

    // Setup cookie for managing temporary authorization state

    const cookieOptions = {
        encoding: 'iron',
        path: '/',
        password: settings.password,
        isSecure: settings.isSecure !== false,                  // Defaults to true
        isHttpOnly: settings.isHttpOnly !== false,              // Defaults to true
        ttl: settings.ttl,
        domain: settings.domain,
        ignoreErrors: true,
        clearInvalid: true
    };

    settings.cookie = settings.cookie || 'bell-' + settings.name;
    server.state(settings.cookie, cookieOptions);

    if (internals.simulate) {
        return internals.simulated(settings);
    }

    return { authenticate: (settings.provider.protocol === 'oauth' ? OAuth.v1 : OAuth.v2)(settings) };
};
Example #12
0
internals.Client = function (options = {}) {

    Hoek.assert(!options.agents || (options.agents.https && options.agents.http && options.agents.httpsAllowUnauthorized), 'Option agents must include "http", "https", and "httpsAllowUnauthorized"');

    this._defaults = Hoek.cloneWithShallow(options, internals.shallowOptions);

    this.agents = this._defaults.agents || {
        https: new Https.Agent({ maxSockets: Infinity }),
        http: new Http.Agent({ maxSockets: Infinity }),
        httpsAllowUnauthorized: new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false })
    };

    if (!options.events) {
        return;
    }

    this.events = new Events.EventEmitter();
    this._emit = function (...args) {

        this.events.emit(...args);
    };
};
Example #13
0
exports = module.exports = internals.Route = function (options, connection, plugin) {

    // Apply plugin environment (before schema validation)

    var realm = plugin.realm;
    if (realm.modifiers.route.vhost ||
        realm.modifiers.route.prefix) {

        options = Hoek.cloneWithShallow(options, ['config']);       // config is left unchanged
        options.path = (realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (options.path !== '/' ? options.path : '') : options.path);
        options.vhost = realm.modifiers.route.vhost || options.vhost;
    }

    // Setup and validate route configuration

    Hoek.assert(options.path, 'Route missing path');
    Hoek.assert(options.handler || (options.config && options.config.handler), 'Missing or undefined handler:', options.method, options.path);
    Hoek.assert(!!options.handler ^ !!(options.config && options.config.handler), 'Handler must only appear once:', options.method, options.path);            // XOR
    Hoek.assert(options.path === '/' || options.path[options.path.length - 1] !== '/' || !connection.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when connection configured to strip:', options.method, options.path);
    Hoek.assert(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/.test(options.method), 'Invalid method name:', options.method, options.path);

    options = Schema.apply('route', options, options.path);

    var handler = options.handler || options.config.handler;
    var method = options.method.toLowerCase();
    Hoek.assert(method !== 'head', 'Method name not allowed:', options.method, options.path);

    // Apply settings in order: {connection} <- {handler} <- {realm} <- {route}

    var handlerDefaults = Handler.defaults(method, handler, connection.server);
    var base = Hoek.applyToDefaultsWithShallow(connection.settings.routes, handlerDefaults, ['bind']);
    base = Hoek.applyToDefaultsWithShallow(base, realm.settings, ['bind']);
    this.settings = Hoek.applyToDefaultsWithShallow(base, options.config || {}, ['bind']);
    this.settings.handler = handler;
    this.settings = Schema.apply('routeConfig', this.settings, options.path);

    var socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket);
    Hoek.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout:', options.path);
    Hoek.assert(!this.settings.payload.timeout || !socketTimeout || this.settings.payload.timeout < socketTimeout, 'Payload timeout must be shorter than socket timeout:', options.path);

    this.connection = connection;
    this.server = connection.server;
    this.path = options.path;
    this.method = method;
    this.plugin = plugin;

    this.public = {
        method: this.method,
        path: this.path,
        vhost: this.vhost,
        realm: this.plugin.realm,
        settings: this.settings
    };

    this.settings.vhost = options.vhost;
    this.settings.plugins = this.settings.plugins || {};            // Route-specific plugins settings, namespaced using plugin name
    this.settings.app = this.settings.app || {};                    // Route-specific application settings

    // Path parsing

    this._analysis = this.connection._router.analyze(this.path);
    this.params = this._analysis.params;
    this.fingerprint = this._analysis.fingerprint;

    // Validation

    var validation = this.settings.validate;
    if (this.method === 'get') {

        // Assert on config, not on merged settings

        Hoek.assert(!options.config || !options.config.payload, 'Cannot set payload settings on HEAD or GET request:', options.path);
        Hoek.assert(!options.config || !options.config.validate || !options.config.validate.payload, 'Cannot validate HEAD or GET requests:', options.path);

        validation.payload = null;
    }

    ['headers', 'params', 'query', 'payload'].forEach(function (type) {

        validation[type] = internals.compileRule(validation[type]);
    });

    if (this.settings.response.schema !== undefined ||
        this.settings.response.status) {

        var rule = this.settings.response.schema;
        this.settings.response.status = this.settings.response.status || {};
        var statuses = Object.keys(this.settings.response.status);

        if (rule === true && !statuses.length) {

            this.settings.response = null;
        }
        else {
            this.settings.response.schema = internals.compileRule(rule);
            for (var i = 0, il = statuses.length; i < il; ++i) {
                var code = statuses[i];
                this.settings.response.status[code] = internals.compileRule(this.settings.response.status[code]);
            }
        }
    }
    else {
        this.settings.response = null;
    }

    // Payload parsing

    if (this.method === 'get') {

        this.settings.payload = null;
    }
    else {
        if (this.settings.payload.allow) {
            this.settings.payload.allow = [].concat(this.settings.payload.allow);
        }
    }

    Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', options.method, options.path);
    Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', options.path);

    // Authentication configuration

    this.settings.auth = this.connection.auth._setupRoute(this.settings.auth, options.path);

    // Cache

    if (this.method === 'get' &&
        (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) {

        this.settings.cache._statuses = Hoek.mapToObject(this.settings.cache.statuses);
        this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt });
    }

    // CORS

    if (this.settings.cors) {
        this.settings.cors = Hoek.applyToDefaults(Defaults.cors, this.settings.cors);

        var cors = this.settings.cors;
        this.settings._cors = {
            headers: cors.headers.concat(cors.additionalHeaders).join(','),
            methods: cors.methods.concat(cors.additionalMethods).join(','),
            exposedHeaders: cors.exposedHeaders.concat(cors.additionalExposedHeaders).join(',')
        };

        var _cors = this.settings._cors;

        if (cors.origin.length) {
            _cors.origin = {
                any: false,
                qualified: [],
                qualifiedString: '',
                wildcards: []
            };

            if (cors.origin.indexOf('*') !== -1) {
                Hoek.assert(cors.origin.length === 1, 'Cannot specify cors.origin * together with other values');
                _cors.origin.any = true;
            }
            else {
                for (var c = 0, cl = cors.origin.length; c < cl; ++c) {
                    var origin = cors.origin[c];
                    if (origin.indexOf('*') !== -1) {
                        _cors.origin.wildcards.push(new RegExp('^' + Hoek.escapeRegex(origin).replace(/\\\*/g, '.*').replace(/\\\?/g, '.') + '$'));
                    }
                    else {
                        _cors.origin.qualified.push(origin);
                    }
                }

                Hoek.assert(cors.matchOrigin || !_cors.origin.wildcards.length, 'Cannot include wildcard origin values with matchOrigin disabled');
                _cors.origin.qualifiedString = _cors.origin.qualified.join(' ');
            }
        }
    }

    // Security

    if (this.settings.security) {
        this.settings.security = Hoek.applyToDefaults(Defaults.security, this.settings.security);

        var security = this.settings.security;
        if (security.hsts) {
            if (security.hsts === true) {
                security._hsts = 'max-age=15768000';
            }
            else if (typeof security.hsts === 'number') {
                security._hsts = 'max-age=' + security.hsts;
            }
            else {
                security._hsts = 'max-age=' + (security.hsts.maxAge || 15768000);
                if (security.hsts.includeSubdomains || security.hsts.includeSubDomains) {
                    security._hsts += '; includeSubDomains';
                }
                if (security.hsts.preload) {
                    security._hsts += '; preload';
                }
            }
        }

        if (security.xframe) {
            if (security.xframe === true) {
                security._xframe = 'DENY';
            }
            else if (typeof security.xframe === 'string') {
                security._xframe = security.xframe.toUpperCase();
            }
            else if (security.xframe.rule === 'allow-from') {
                if (!security.xframe.source) {
                    security._xframe = 'SAMEORIGIN';
                }
                else {
                    security._xframe = 'ALLOW-FROM ' + security.xframe.source;
                }
            }
            else {
                security._xframe = security.xframe.rule.toUpperCase();
            }
        }
    }

    // Handler

    this.settings.handler = Handler.configure(this.settings.handler, this);
    this._prerequisites = Handler.prerequisites(this.settings.pre, this.server);

    // Route lifecycle

    this._extensions = {
        onPreAuth: this._combineExtensions('onPreAuth'),
        onPostAuth: this._combineExtensions('onPostAuth'),
        onPreHandler: this._combineExtensions('onPreHandler'),
        onPostHandler: this._combineExtensions('onPostHandler'),
        onPreResponse: this._combineExtensions('onPreResponse')
    };

    this._cycle = null;
    this.rebuild();
};
Example #14
0
    constructor(route, server, options = {}) {

        const core = server._core;
        const realm = server.realm;

        // Apply plugin environment (before schema validation)

        if (realm.modifiers.route.vhost ||
            realm.modifiers.route.prefix) {

            route = Hoek.cloneWithShallow(route, ['options']);       // options is left unchanged
            route.path = (realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (route.path !== '/' ? route.path : '') : route.path);
            route.vhost = realm.modifiers.route.vhost || route.vhost;
        }

        // Setup and validate route configuration

        Hoek.assert(route.path, 'Route missing path');
        const routeDisplay = route.method + ' ' + route.path;

        let config = route.options || route.config;
        if (typeof config === 'function') {
            config = config.call(realm.settings.bind, server);
        }

        Hoek.assert(route.handler || (config && config.handler), 'Missing or undefined handler:', routeDisplay);
        Hoek.assert(!!route.handler ^ !!(config && config.handler), 'Handler must only appear once:', routeDisplay);            // XOR
        Hoek.assert(route.path === '/' || route.path[route.path.length - 1] !== '/' || !core.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when configured to strip:', routeDisplay);

        config = Config.enable(config);
        route = Config.apply('route', route, routeDisplay);

        const handler = route.handler || config.handler;
        const method = route.method.toLowerCase();
        Hoek.assert(method !== 'head', 'Method name not allowed:', routeDisplay);

        // Apply settings in order: {server} <- {handler} <- {realm} <- {route}

        const handlerDefaults = Handler.defaults(method, handler, core);
        let base = Hoek.applyToDefaultsWithShallow(core.settings.routes, handlerDefaults, ['bind']);
        base = Hoek.applyToDefaultsWithShallow(base, realm.settings, ['bind']);
        this.settings = Hoek.applyToDefaultsWithShallow(base, config, ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query']);
        this.settings.handler = handler;
        this.settings = Config.apply('routeConfig', this.settings, routeDisplay);

        const socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket);
        Hoek.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout:', routeDisplay);
        Hoek.assert(!this.settings.payload.timeout || !socketTimeout || this.settings.payload.timeout < socketTimeout, 'Payload timeout must be shorter than socket timeout:', routeDisplay);

        this._core = core;
        this.path = route.path;
        this.method = method;
        this.realm = realm;

        this.settings.vhost = route.vhost;
        this.settings.plugins = this.settings.plugins || {};            // Route-specific plugins settings, namespaced using plugin name
        this.settings.app = this.settings.app || {};                    // Route-specific application settings

        // Path parsing

        this._special = !!options.special;
        this._analysis = this._core.router.analyze(this.path);
        this.params = this._analysis.params;
        this.fingerprint = this._analysis.fingerprint;

        this.public = {
            method: this.method,
            path: this.path,
            vhost: this.vhost,
            realm,
            settings: this.settings,
            fingerprint: this.fingerprint,
            auth: {
                access: (request) => Auth.testAccess(request, this.public)
            }
        };

        // Validation

        const validation = this.settings.validate;
        if (this.method === 'get') {

            // Assert on config, not on merged settings

            Hoek.assert(!config.payload, 'Cannot set payload settings on HEAD or GET request:', routeDisplay);
            Hoek.assert(!config.validate || !config.validate.payload, 'Cannot validate HEAD or GET requests:', routeDisplay);

            validation.payload = null;
        }

        Hoek.assert(!validation.params || this.params.length, 'Cannot set path parameters validations without path parameters:', routeDisplay);

        ['headers', 'params', 'query', 'payload'].forEach((type) => {

            validation[type] = Validation.compile(validation[type]);
        });

        if (this.settings.response.schema !== undefined ||
            this.settings.response.status) {

            this.settings.response._validate = true;

            const rule = this.settings.response.schema;
            this.settings.response.status = this.settings.response.status || {};
            const statuses = Object.keys(this.settings.response.status);

            if (rule === true &&
                !statuses.length) {

                this.settings.response._validate = false;
            }
            else {
                this.settings.response.schema = Validation.compile(rule);
                for (let i = 0; i < statuses.length; ++i) {
                    const code = statuses[i];
                    this.settings.response.status[code] = Validation.compile(this.settings.response.status[code]);
                }
            }
        }

        // Payload parsing

        if (this.method === 'get') {
            this.settings.payload = null;
        }
        else {
            this.settings.payload.decoders = this._core.compression._decoders;        // Reference the shared object to keep up to date
        }

        Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', routeDisplay);
        Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', routeDisplay);

        // Authentication configuration

        this.settings.auth = (this._special ? false : this._core.auth._setupRoute(this.settings.auth, route.path));

        // Cache

        if (this.method === 'get' &&
            typeof this.settings.cache === 'object' &&
            (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) {

            this.settings.cache._statuses = Hoek.mapToObject(this.settings.cache.statuses);
            this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt });
        }

        // CORS

        this.settings.cors = Cors.route(this.settings.cors);

        // Security

        this.settings.security = Security.route(this.settings.security);

        // Handler

        this.settings.handler = Handler.configure(this.settings.handler, this);
        this._prerequisites = Handler.prerequisitesConfig(this.settings.pre);

        // Route lifecycle

        this._extensions = {
            onPreResponse: Ext.combine(this, 'onPreResponse')
        };

        if (this._special) {
            this._cycle = [internals.drain, Handler.execute];
            this.rebuild();
            return;
        }

        this._extensions.onPreAuth = Ext.combine(this, 'onPreAuth');
        this._extensions.onCredentials = Ext.combine(this, 'onCredentials');
        this._extensions.onPostAuth = Ext.combine(this, 'onPostAuth');
        this._extensions.onPreHandler = Ext.combine(this, 'onPreHandler');
        this._extensions.onPostHandler = Ext.combine(this, 'onPostHandler');

        this.rebuild();
    }
Example #15
0
internals.Methods.prototype._add = function (name, fn, options, env) {

    var self = this;

    Hoek.assert(typeof fn === 'function', 'fn must be a function');
    Hoek.assert(typeof name === 'string', 'name must be a string');
    Hoek.assert(name.match(exports.methodNameRx), 'Invalid name:', name);
    Hoek.assert(!Hoek.reach(this.methods, name, { functions: false }), 'Server method function name already exists');

    options = options || {};
    Schema.assert('method', options, name);

    var settings = Hoek.cloneWithShallow(options, ['bind']);
    settings.generateKey = settings.generateKey || internals.generateKey;

    var bind = settings.bind || (env && env.bind) || null;

    // Create method

    settings.cache = settings.cache || {};
    settings.cache.generateFunc = function (id, next) {

        id.args[id.args.length - 1] = next;                 // function (err, result, ttl)
        fn.apply(bind, id.args);
    };

    var cache = (settings.cache.expiresIn || settings.cache.expiresAt ? this.pack._provisionCache(settings.cache, 'method', name, settings.cache.segment)
                                                                      : new Catbox.Policy(settings.cache));

    var method = function (/* arguments, methodNext */) {

        var args = arguments;
        var methodNext = args[args.length - 1];

        var key = settings.generateKey.apply(bind, args);
        if (key === null || typeof key !== 'string') {                             // Value can be ''
            self.pack.log(['hapi', 'method', 'key', 'error'], { name: name, args: args, key: key });
            key = null;
        }

        cache.get({ id: key, args: args }, methodNext);
    };

    method.cache = {
        drop: function (/* arguments, callback */) {

            var dropCallback = arguments[arguments.length - 1];

            var key = settings.generateKey.apply(null, arguments);
            if (key === null) {                             // Value can be ''
                return Hoek.nextTick(dropCallback)(Boom.badImplementation('Invalid method key'));
            }

            return cache.drop(key, dropCallback);
        }
    };

    // create method path

    var path = name.split('.');
    var ref = this.methods;
    for (var i = 0, il = path.length; i < il; ++i) {
        if (!ref[path[i]]) {
            ref[path[i]] = (i + 1 === il ? method : {});
        }

        ref = ref[path[i]];
    }
};
Example #16
0
File: route.js Project: 0x00xw/hapi
exports = module.exports = internals.Route = function (route, connection, plugin, options) {

    options = options || {};

    // Apply plugin environment (before schema validation)

    const realm = plugin.realm;
    if (realm.modifiers.route.vhost ||
        realm.modifiers.route.prefix) {

        route = Hoek.cloneWithShallow(route, ['config']);       // config is left unchanged
        route.path = (realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (route.path !== '/' ? route.path : '') : route.path);
        route.vhost = realm.modifiers.route.vhost || route.vhost;
    }

    // Setup and validate route configuration

    Hoek.assert(route.path, 'Route missing path');
    const routeDisplay = route.method + ' ' + route.path;
    Hoek.assert(route.handler || (route.config && route.config.handler), 'Missing or undefined handler:', routeDisplay);
    Hoek.assert(!!route.handler ^ !!(route.config && route.config.handler), 'Handler must only appear once:', routeDisplay);            // XOR
    Hoek.assert(route.path === '/' || route.path[route.path.length - 1] !== '/' || !connection.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when connection configured to strip:', routeDisplay);

    route = Schema.apply('route', route, routeDisplay);

    const handler = route.handler || route.config.handler;
    const method = route.method.toLowerCase();
    Hoek.assert(method !== 'head', 'Method name not allowed:', routeDisplay);

    // Apply settings in order: {connection} <- {handler} <- {realm} <- {route}

    const handlerDefaults = Handler.defaults(method, handler, connection.server);
    let base = Hoek.applyToDefaultsWithShallow(connection.settings.routes, handlerDefaults, ['bind']);
    base = Hoek.applyToDefaultsWithShallow(base, realm.settings, ['bind']);
    this.settings = Hoek.applyToDefaultsWithShallow(base, route.config || {}, ['bind']);
    this.settings.handler = handler;
    this.settings = Schema.apply('routeConfig', this.settings, routeDisplay);

    const socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket);
    Hoek.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout:', routeDisplay);
    Hoek.assert(!this.settings.payload.timeout || !socketTimeout || this.settings.payload.timeout < socketTimeout, 'Payload timeout must be shorter than socket timeout:', routeDisplay);

    this.connection = connection;
    this.server = connection.server;
    this.path = route.path;
    this.method = method;
    this.plugin = plugin;

    this.settings.vhost = route.vhost;
    this.settings.plugins = this.settings.plugins || {};            // Route-specific plugins settings, namespaced using plugin name
    this.settings.app = this.settings.app || {};                    // Route-specific application settings

    // Path parsing

    this._special = !!options.special;
    this._analysis = this.connection._router.analyze(this.path);
    this.params = this._analysis.params;
    this.fingerprint = this._analysis.fingerprint;

    this.public = {
        method: this.method,
        path: this.path,
        vhost: this.vhost,
        realm: this.plugin.realm,
        settings: this.settings,
        fingerprint: this.fingerprint
    };

    // Validation

    const validation = this.settings.validate;
    if (this.method === 'get') {

        // Assert on config, not on merged settings

        Hoek.assert(!route.config || !route.config.payload, 'Cannot set payload settings on HEAD or GET request:', routeDisplay);
        Hoek.assert(!route.config || !route.config.validate || !route.config.validate.payload, 'Cannot validate HEAD or GET requests:', routeDisplay);

        validation.payload = null;
    }

    ['headers', 'params', 'query', 'payload'].forEach((type) => {

        validation[type] = Validation.compile(validation[type]);
    });

    if (this.settings.response.schema !== undefined ||
        this.settings.response.status) {

        this.settings.response._validate = true;

        const rule = this.settings.response.schema;
        this.settings.response.status = this.settings.response.status || {};
        const statuses = Object.keys(this.settings.response.status);

        if (rule === true &&
            !statuses.length) {

            this.settings.response._validate = false;
        }
        else {
            this.settings.response.schema = Validation.compile(rule);
            for (let i = 0; i < statuses.length; ++i) {
                const code = statuses[i];
                this.settings.response.status[code] = Validation.compile(this.settings.response.status[code]);
            }
        }
    }

    // Payload parsing

    if (this.method === 'get') {
        this.settings.payload = null;
    }
    else {
        if (this.settings.payload.allow) {
            this.settings.payload.allow = [].concat(this.settings.payload.allow);
        }
    }

    Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', routeDisplay);
    Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', routeDisplay);

    // Authentication configuration

    this.settings.auth = (this._special ? false : this.connection.auth._setupRoute(this.settings.auth, route.path));

    // Cache

    if (this.method === 'get' &&
        (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) {

        this.settings.cache._statuses = Hoek.mapToObject(this.settings.cache.statuses);
        this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt });
    }

    // CORS

    this.settings.cors = Cors.route(this.settings.cors);

    // Security

    if (this.settings.security) {
        this.settings.security = Hoek.applyToDefaults(Defaults.security, this.settings.security);

        const security = this.settings.security;
        if (security.hsts) {
            if (security.hsts === true) {
                security._hsts = 'max-age=15768000';
            }
            else if (typeof security.hsts === 'number') {
                security._hsts = 'max-age=' + security.hsts;
            }
            else {
                security._hsts = 'max-age=' + (security.hsts.maxAge || 15768000);
                if (security.hsts.includeSubdomains || security.hsts.includeSubDomains) {
                    security._hsts = security._hsts + '; includeSubDomains';
                }
                if (security.hsts.preload) {
                    security._hsts = security._hsts + '; preload';
                }
            }
        }

        if (security.xframe) {
            if (security.xframe === true) {
                security._xframe = 'DENY';
            }
            else if (typeof security.xframe === 'string') {
                security._xframe = security.xframe.toUpperCase();
            }
            else if (security.xframe.rule === 'allow-from') {
                if (!security.xframe.source) {
                    security._xframe = 'SAMEORIGIN';
                }
                else {
                    security._xframe = 'ALLOW-FROM ' + security.xframe.source;
                }
            }
            else {
                security._xframe = security.xframe.rule.toUpperCase();
            }
        }
    }

    // Handler

    this.settings.handler = Handler.configure(this.settings.handler, this);
    this._prerequisites = Handler.prerequisitesConfig(this.settings.pre, this.server);

    // Route lifecycle

    this._extensions = {
        onPreResponse: this._combineExtensions('onPreResponse')
    };

    if (this._special) {
        this._cycle = [Handler.execute];
        return;
    }

    this._extensions.onPreAuth = this._combineExtensions('onPreAuth');
    this._extensions.onPostAuth = this._combineExtensions('onPostAuth');
    this._extensions.onPreHandler = this._combineExtensions('onPreHandler');
    this._extensions.onPostHandler = this._combineExtensions('onPostHandler');

    this.rebuild();
};
Example #17
0
internals.Methods.prototype._add = function (name, method, options, realm) {

    var self = this;

    Hoek.assert(typeof method === 'function', 'method must be a function');
    Hoek.assert(typeof name === 'string', 'name must be a string');
    Hoek.assert(name.match(exports.methodNameRx), 'Invalid name:', name);
    Hoek.assert(!Hoek.reach(this.methods, name, { functions: false }), 'Server method function name already exists');

    options = options || {};
    Schema.assert('method', options, name);

    var settings = Hoek.cloneWithShallow(options, ['bind']);
    settings.generateKey = settings.generateKey || internals.generateKey;
    var bind = settings.bind || realm.settings.bind || null;
    var bound = bind ? function () { return method.apply(bind, arguments); } : method;

    // Normalize methods

    var normalized = bound;
    if (settings.callback === false) {                                          // Defaults to true
        normalized = function (/* arg1, arg2, ..., argn, methodNext */) {

            var args = [];
            for (var i = 0, il = arguments.length; i < il - 1; ++i) {
                args.push(arguments[i]);
            }

            var methodNext = arguments[il - 1];

            var result = null;
            var error = null;

            try {
                result = method.apply(bind, args);
            }
            catch (err) {
                error = err;
            }

            if (result instanceof Error) {
                error = result;
                result = null;
            }

            if (error ||
                typeof result !== 'object' ||
                typeof result.then !== 'function') {

                return methodNext(error, result);
            }

            // Promise object

            var onFulfilled = function (result) {

                return methodNext(null, result);
            };

            var onRejected = function (err) {

                return methodNext(err);
            };

            result.then(onFulfilled, onRejected);
        };
    }

    // Not cached

    if (!settings.cache) {
        return this._assign(name, bound, normalized);
    }

    // Cached

    Hoek.assert(!settings.cache.generateFunc, 'Cannot set generateFunc with method caching');

    settings.cache.generateFunc = function (id, next) {

        id.args.push(next);                     // function (err, result, ttl)
        normalized.apply(bind, id.args);
    };

    var cache = this.server.cache(settings.cache, '#' + name);

    var func = function (/* arguments, methodNext */) {

        var args = [];
        for (var i = 0, il = arguments.length; i < il - 1; ++i) {
            args.push(arguments[i]);
        }

        var methodNext = arguments[il - 1];

        var key = settings.generateKey.apply(bind, args);
        if (key === null ||                                 // Value can be ''
            typeof key !== 'string') {                      // When using custom generateKey

            return Hoek.nextTick(methodNext)(Boom.badImplementation('Invalid method key when invoking: ' + name, { name: name, args: args }));
        }

        cache.get({ id: key, args: args }, methodNext);
    };

    func.cache = {
        drop: function (/* arguments, callback */) {

            var args = [];
            for (var i = 0, il = arguments.length; i < il - 1; ++i) {
                args.push(arguments[i]);
            }

            var methodNext = arguments[il - 1];

            var key = settings.generateKey.apply(null, args);
            if (key === null) {                             // Value can be ''
                return Hoek.nextTick(methodNext)(Boom.badImplementation('Invalid method key'));
            }

            return cache.drop(key, methodNext);
        }
    };

    this._assign(name, func, func);
};
Example #18
0
exports = module.exports = internals.Route = function (options, server, env) {

    // Apply plugin environment (before schema validation)

    if (env &&
        (env.route.vhost || env.route.prefix)) {

        options = Hoek.cloneWithShallow(options, ['config']);
        options.path = (env.route.prefix ? env.route.prefix + (options.path !== '/' ? options.path : '') : 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 === '/' || 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.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.path = options.path;
    this.method = options.method.toLowerCase();
    this._env = env || {};                                          // Plugin-specific environment

    this.settings.method = this.method;                             // Expose method in settings
    this.settings.path = this.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

    // Path parsing

    this._analysis = this.server._router.analyze(this.path);
    this.params = this._analysis.params;
    this.fingerprint = this._analysis.fingerprint;

    // Validation

    this.settings.validate = this.settings.validate || {};
    var validation = this.settings.validate;
    ['headers', 'params', 'query', 'payload'].forEach(function (type) {

        // null, undefined, true - anything allowed
        // false - nothing allowed
        // {...} - ... allowed

        var rule = validation[type];
        validation[type] = (rule === false ? Joi.object({})
                                           : typeof rule === 'function' ? rule
                                                                        : !rule || rule === true ? null                     // false tested earlier
                                                                                                 : Joi.compile(rule));
    });

    if (this.settings.response) {
        var rule = this.settings.response.schema;
        if (rule === true ||
            this.settings.response.sample === 0) {

            this.settings.response = null;
        }
        else {
            this.settings.response.schema = (rule === false ? Joi.object({})
                                                            : typeof rule === 'function' ? rule
                                                                                         : Joi.compile(rule));
        }
    }

    // Payload parsing

    if (this.method !== 'get' &&
        this.method !== 'head') {

        this.settings.payload = this.settings.payload || {};
        var isProxy = (typeof this.settings.handler === 'object' && !!this.settings.handler.proxy);
        this.settings.payload.output = this.settings.payload.output || (isProxy ? 'stream' : 'data');
        this.settings.payload.parse = this.settings.payload.parse !== undefined ? this.settings.payload.parse : !isProxy;
        this.settings.payload.maxBytes = this.settings.payload.maxBytes || this.server.settings.payload.maxBytes;
        this.settings.payload.uploads = this.settings.payload.uploads || this.server.settings.payload.uploads;
        this.settings.payload.failAction = this.settings.payload.failAction || 'error';
        if (this.settings.payload.allow) {
            this.settings.payload.allow = [].concat(this.settings.payload.allow);
        }
    }
    else {
        Hoek.assert(!this.settings.payload, 'Cannot set payload settings on HEAD or GET request:', options.path);
        Hoek.assert(!this.settings.validate.payload, 'Cannot validate HEAD or GET requests:', options.path);
    }

    Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', options.path);
    Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', options.path);

    // Authentication configuration

    this.settings.auth = this.server.auth._setupRoute(this.settings.auth, options.path);

    // Cache

    Hoek.assert(!this.settings.cache || this.method === 'get', 'Only GET routes can use a cache:', options.path);
    this._cache = this.settings.cache ? new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt }) : null;

    // Handler

    this.settings.handler = Handler.configure(this.settings.handler, this);
    this._prerequisites = Handler.prerequisites(this.settings.pre, server);

    // Route lifecycle

    this._cycle = this.lifecycle();
};