Plato on Github
Report Home
lib/connection.js
Maintainability
64.44
Lines of code
421
Difficulty
57.53
Estimated Errors
5.01
Function weight
By Complexity
By SLOC
'use strict'; // Load modules const Events = require('events'); const Http = require('http'); const Https = require('https'); const Os = require('os'); const Path = require('path'); const Boom = require('boom'); const Call = require('call'); const Hoek = require('hoek'); const Shot = require('shot'); const Statehood = require('statehood'); const Auth = require('./auth'); const Cors = require('./cors'); const Ext = require('./ext'); const Promises = require('./promises'); const Route = require('./route'); // Declare internals const internals = { counter: { min: 10000, max: 99999 } }; exports = module.exports = internals.Connection = function (server, options) { const now = Date.now(); Events.EventEmitter.call(this); this.settings = options; // options cloned in server.connection() this.server = server; // Normalize settings this.settings.labels = Hoek.unique(this.settings.labels || []); // Remove duplicates if (this.settings.port === undefined) { this.settings.port = 0; } this.type = (typeof this.settings.port === 'string' ? 'socket' : 'tcp'); if (this.type === 'socket') { this.settings.port = (this.settings.port.indexOf('/') !== -1 ? Path.resolve(this.settings.port) : this.settings.port.toLowerCase()); } if (this.settings.autoListen === undefined) { this.settings.autoListen = true; } Hoek.assert(this.settings.autoListen || !this.settings.port, 'Cannot specify port when autoListen is false'); Hoek.assert(this.settings.autoListen || !this.settings.address, 'Cannot specify address when autoListen is false'); // Connection facilities this._started = false; this._connections = {}; this._onConnection = null; // Used to remove event listener on stop this.registrations = {}; // Tracks plugin for dependency validation { name -> { version } } this._extensions = { onRequest: new Ext(this.server), onPreAuth: new Ext(this.server), onPostAuth: new Ext(this.server), onPreHandler: new Ext(this.server), onPostHandler: new Ext(this.server), onPreResponse: new Ext(this.server) }; this._requestCounter = { value: internals.counter.min, min: internals.counter.min, max: internals.counter.max }; this._load = server._heavy.policy(this.settings.load); this.states = new Statehood.Definitions(this.settings.state); this.auth = new Auth(this); this._router = new Call.Router(this.settings.router); this._defaultRoutes(); this.plugins = {}; // Registered plugin APIs by plugin name this.app = {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins // Create listener this.listener = this.settings.listener || (this.settings.tls ? Https.createServer(this.settings.tls) : Http.createServer()); this.listener.on('request', this._dispatch()); this._init(); this.listener.on('clientError', (err, socket) => { this.server._log(['connection', 'client', 'error'], err); }); // Connection information this.info = { created: now, started: 0, host: this.settings.host || Os.hostname() || 'localhost', port: this.settings.port, protocol: this.type === 'tcp' ? (this.settings.tls ? 'https' : 'http') : this.type, id: Os.hostname() + ':' + process.pid + ':' + now.toString(36) }; this.info.uri = (this.settings.uri || (this.info.protocol + ':' + (this.type === 'tcp' ? '//' + this.info.host + (this.info.port ? ':' + this.info.port : '') : this.info.port))); this.on('route', Cors.options); }; Hoek.inherits(internals.Connection, Events.EventEmitter); internals.Connection.prototype._init = function () { // Setup listener this.listener.once('listening', () => { // Update the address, port, and uri with active values if (this.type === 'tcp') { const address = this.listener.address(); this.info.address = address.address; this.info.port = address.port; this.info.uri = (this.settings.uri || (this.info.protocol + '://' + this.info.host + ':' + this.info.port)); } this._onConnection = (connection) => { const key = connection.remoteAddress + ':' + connection.remotePort; this._connections[key] = connection; connection.once('close', () => { delete this._connections[key]; }); }; this.listener.on('connection', this._onConnection); }); }; internals.Connection.prototype._start = function (callback) { if (this._started) { return process.nextTick(callback); } this._started = true; this.info.started = Date.now(); if (!this.settings.autoListen) { return process.nextTick(callback); } const onError = (err) => { this._started = false; return callback(err); }; this.listener.once('error', onError); const finalize = () => { this.listener.removeListener('error', onError); callback(); }; if (this.type !== 'tcp') { this.listener.listen(this.settings.port, finalize); } else { const address = this.settings.address || this.settings.host || '0.0.0.0'; this.listener.listen(this.settings.port, address, finalize); } }; internals.Connection.prototype._stop = function (options, callback) { if (!this._started) { return process.nextTick(callback); } this._started = false; this.info.started = 0; const timeoutId = setTimeout(() => { Object.keys(this._connections).forEach((key) => { this._connections[key].destroy(); }); this._connections = {}; }, options.timeout); this.listener.close(() => { this.listener.removeListener('connection', this._onConnection); clearTimeout(timeoutId); this._init(); return callback(); }); // Tell idle keep-alive connections to close Object.keys(this._connections).forEach((key) => { const connection = this._connections[key]; if (!connection._isHapiProcessing) { connection.end(); } }); }; internals.Connection.prototype._dispatch = function (options) { options = options || {}; return (req, res) => { // Track socket request processing state if (req.socket) { req.socket._isHapiProcessing = true; res.on('finish', () => { req.socket._isHapiProcessing = false; if (!this._started) { req.socket.end(); } }); } // Create request const request = this.server._requestor.request(this, req, res, options); // Check load const overload = this._load.check(); if (overload) { this.server._log(['load'], this.server.load); request._reply(overload); } else { // Execute request lifecycle request._protect.enter(() => { request._execute(); }); } }; }; internals.Connection.prototype.inject = function (options, callback) { if (!callback) { return Promises.wrap(this, this.inject, [options]); } let settings = options; if (typeof settings === 'string') { settings = { url: settings }; } if (!settings.authority || settings.credentials || settings.app || settings.plugins || settings.allowInternals !== undefined) { // Can be false settings = Hoek.shallow(settings); // options can be reused delete settings.credentials; delete settings.artifacts; // Cannot appear without credentials delete settings.app; delete settings.plugins; delete settings.allowInternals; settings.authority = settings.authority || (this.info.host + ':' + this.info.port); } const needle = this._dispatch({ credentials: options.credentials, artifacts: options.artifacts, allowInternals: options.allowInternals, app: options.app, plugins: options.plugins }); Shot.inject(needle, settings, (res) => { if (res.raw.res._hapi) { res.result = res.raw.res._hapi.result; res.request = res.raw.res._hapi.request; delete res.raw.res._hapi; } if (res.result === undefined) { res.result = res.payload; } return callback(res); }); }; internals.Connection.prototype.table = function (host) { return this._router.table(host); }; internals.Connection.prototype.lookup = function (id) { Hoek.assert(id && typeof id === 'string', 'Invalid route id:', id); const record = this._router.ids[id]; if (!record) { return null; } return record.route.public; }; internals.Connection.prototype.match = function (method, path, host) { Hoek.assert(method && typeof method === 'string', 'Invalid method:', method); Hoek.assert(path && typeof path === 'string' && path[0] === '/', 'Invalid path:', path); Hoek.assert(!host || typeof host === 'string', 'Invalid host:', host); const match = this._router.route(method.toLowerCase(), path, host); Hoek.assert(match !== this._router.specials.badRequest, 'Invalid path:', path); if (match === this._router.specials.notFound) { return null; } return match.route.public; }; internals.Connection.prototype._ext = function (event) { const type = event.type; Hoek.assert(this._extensions[type], 'Unknown event type', type); this._extensions[type].add(event); }; internals.Connection.prototype._route = function (configs, plugin) { configs = [].concat(configs); for (let i = 0; i < configs.length; ++i) { const config = configs[i]; if (Array.isArray(config.method)) { for (let j = 0; j < config.method.length; ++j) { const method = config.method[j]; const settings = Hoek.shallow(config); settings.method = method; this._addRoute(settings, plugin); } } else { this._addRoute(config, plugin); } } }; internals.Connection.prototype._addRoute = function (config, plugin) { const route = new Route(config, this, plugin); // Do no use config beyond this point, use route members const vhosts = [].concat(route.settings.vhost || '*'); for (let i = 0; i < vhosts.length; ++i) { const vhost = vhosts[i]; const record = this._router.add({ method: route.method, path: route.path, vhost: vhost, analysis: route._analysis, id: route.settings.id }, route); route.fingerprint = record.fingerprint; route.params = record.params; } this.emit('route', route.public, this, plugin); }; internals.Connection.prototype._defaultRoutes = function () { this._router.special('notFound', new Route({ method: '_special', path: '/{p*}', handler: internals.notFound }, this, this.server, { special: true })); this._router.special('badRequest', new Route({ method: '_special', path: '/{p*}', handler: internals.badRequest }, this, this.server, { special: true })); if (this.settings.routes.cors) { Cors.handler(this); } }; internals.notFound = function (request, reply) { return reply(Boom.notFound()); }; internals.badRequest = function (request, reply) { return reply(Boom.badRequest()); };