<commit_before>(function() {
    'use strict';

    if (!global.cloudcmd)
        return console.log(
             '# server.js'                                      + '\n'  +
             '# -----------'                                    + '\n'  +
             '# Module is part of Cloud Commander,'             + '\n'  +
             '# easy to use web server.'                        + '\n'  +
             '# http://cloudcmd.io'                             + '\n');

    var main                = global.cloudcmd.main,

        DIR                 = main.DIR,
        LIBDIR              = main.LIBDIR,
        SRVDIR              = main.SRVDIR,

        URL                 = main.url,
        Path                = main.path,
        Querystring         = main.querystring,

        Minify              = main.minify,
        CloudFunc           = main.cloudfunc,
        AppCache            = main.appcache,
        Socket              = main.socket,
        Console             = main.console,
        Terminal            = main.terminal,

        zlib                = main.zlib,
        http                = main.http,
        https               = main.https,
        Util                = main.util,
        express             = main.express,
        expressApp,
        files               = main.files,

        Server, Rest, Route;

    /* базовая инициализация  */
    function init(pAppCachProcessing) {
        var config = main.config;

        /* создаём файл app cache */
        if (config.appcache  && AppCache && config.server )
            Util.exec( pAppCachProcessing );
    }

    /**
     * start server function
     * @param pConfig
     * @param pProcessing {index, appcache, rest}
     */
    function start(options) {
        var redirectServer,
            config         = main.config;

        if (!options)
            options = {};

        Rest            =   options.rest;
        Route           =   options.route;

        init(options.appcache);

        var lPort       =   process.env.PORT            ||  /* c9           */
                            process.env.app_port        ||  /* nodester     */
                            process.env.VCAP_APP_PORT   ||  /* cloudfoundry */
                            config.port,

            lIP         =   process.env.IP              ||  /* c9           */
                            config.ip                   ||
                            (main.WIN32 ? '127.0.0.1' : '0.0.0.0'),

            lSSL        = options.ssl,
            lSSLPort    = config.sslPort,
            lHTTP       = 'http://',
            lHTTPS      = 'https://',

            lSockets    = function(pServer) {
                var listen, msg,
                    status = 'off';

                if (config.socket && Socket) {
                    listen = Socket.listen(pServer);

                    if (listen) {
                        status = 'on';
                        Console.init();
                        Terminal.init();
                    }
                }

                msg     = CloudFunc.formatMsg('sockets', '', status);

                Util.log(msg);
            },

            lHTTPServer = function() {
                expressApp  = express.getApp([
                    Rest,
                    Route,
                    join,
                    controller
                ]);

                Server      = http.createServer(expressApp || respond);

                Server.on('error', Util.log.bind(Util));
                Server.listen(lPort, lIP);
                lServerLog(lHTTP, lPort);
                lSockets(Server);
            },

            lServerLog  = function(http, port) {
                Util.log('* Server running at ' + http + lIP + ':' + port);
            };
        /* server mode or testing mode */
        if (config.server)
            if (lSSL) {
                Util.log('* Redirection http -> https is setted up');
                lServerLog(lHTTP, lPort);

                redirectServer =  http.createServer(function(req, res) {
                    var url,
                        host        = req.headers.host,
                        parsed      = url.parse(lHost),
                        hostName    = parsed.protocol;

                    url             = lHTTPS + hostName + lSSLPort + req.url;

                    main.redirect({
                        response: res,
                        url: url
                    });
                });

                redirectServer.listen(lPort, lIP);

                Server  =  https.createServer(lSSL, respond);
                Server.on('error', function (error) {
                    Util.log('Could not use https port: ' + lSSLPort);
                    Util.log(error);
                    redirectServer.close();
                    Util.log('* Redirection http -> https removed');
                    lHTTPServer();
                });

                lSockets(Server);

                Server.listen(lSSLPort, lIP);
                lServerLog(lHTTPS, lSSLPort);
            } else
                lHTTPServer();
        else
            Util.log('Cloud Commander testing mode');
    }

    function respond(req, res) {
        var i, n, func,
            funcs = ([
                Rest,
                Route,
                join,
                controller
            ]);

        n = funcs.length;
        for (i = 0; i < n; i++) {
            func        = funcs[i];
            funcs[i]    = func.bind(null, req, res);
        }

        Util.loadOnLoad(funcs);
    }

    /**
     * Главная функция, через которую проихсодит
     * взаимодействие, обмен данными с клиентом
     * @param req - запрос клиента (Request)
     * @param res - ответ сервера (Response)
     */
    function controller(pReq, pRes) {
        /* Читаем содержимое папки, переданное в url */
        var lRet, lMin, lCheck, lResult, data,
            lConfig     = main.config,
            parsedUrl   = URL.parse(pReq.url),
            query       = parsedUrl.search || '',
            path        = main.getPathName(pReq),
            lName       = path;

        if (!expressApp)
            Util.log(pReq.method + ' ' + path + query);

        data = {
            name        : path,
            request     : pReq,
            response    : pRes
        };

        /* watching is file changed */
        if (lConfig.appcache)
            AppCache.watch(lName);

        lName   = Path.join(DIR, lName);
        lMin    = lConfig.minify,
        lCheck  = checkExtension(lName);
        lResult = lMin && lCheck;

        Util.ifExec(!lResult,
            function(pParams) {
                var lSendName = pParams && pParams.name || lName;

                main.sendFile({
                    name        : lSendName,
                    cache       : lConfig.cache,
                    gzip        : true,
                    request     : pReq,
                    response    : pRes
                });
        }, function(pCallBack) {
            Minify.optimize(lName, {
                callback    : pCallBack,
                returnName  : true
            });
        });
    }

    function minify(name) {
        return function(callback) {
            Minify.optimize(name, {
                callback    : callback
            });
        };
    }

    function join(request, response, callback) {
        var names, i, n, name, minName, stream, check,
            funcs       = [],
            config      = main.config,
            dir         = DIR,
            gzip        = zlib.createGzip(),
            isGzip      = main.isGZIP(request),
            path        = main.getPathName(request),

            isJoin      = CloudFunc.isJoinURL(path),
            readPipe    = function() {
                main.mainSetHeader({
                    name        : names[0],
                    cache       : config.cache,
                    gzip        : isGzip,
                    request     : request,
                    response    : response
                });

                if (!isGzip)
                    stream = response;
                else
                    stream = gzip;

                files.readPipe({
                    names       : names,
                    dir         : dir,
                    write       : stream,
                    callback    : function(error) {
                        var errorStr;

                        if (error)
                            if (!response.headersSent)
                                main.sendError({
                                    request     : request,
                                    response    : response,
                                    name        : path
                                }, error);
                            else {
                                Util.log(error);
                                errorStr = error.toString();
                                stream.end(errorStr);
                            }
                    }
                });

                /*
                 * pipe should be setted up after
                 * readPipe called with stream param
                 */
                if (isGzip)
                    gzip.pipe(response);
            };

        if (isJoin) {
            names   = CloudFunc.getJoinArray(path);
            n       = names.length;

            if (!config.minify)
                readPipe();
            else {
                for (i = 0; i < n; i++) {
                    name        = Path.join(DIR, names[i]);
                    check       = checkExtension(name);

                    if (check) {
                        minName = Minify.getName(name);

                        if (name !== minName) {
                            names[i] = minName;
                            dir = '';
                        }
                    }

                    funcs.push(minify(name));
                }
                Util.asyncCall(funcs, readPipe);
            }
        } else
            Util.exec(callback);

        return isJoin;
    }

    function checkExtension(name) {
        var ret;

        ret = Util.checkExtension(name, ['.js', '.css', '.html']);

        return ret;
    }

    exports.start           = start;

})();
<commit_msg>chore(server) controller<commit_after>(function() {
    'use strict';

    if (!global.cloudcmd)
        return console.log(
             '# server.js'                                      + '\n'  +
             '# -----------'                                    + '\n'  +
             '# Module is part of Cloud Commander,'             + '\n'  +
             '# easy to use web server.'                        + '\n'  +
             '# http://cloudcmd.io'                             + '\n');

    var main                = global.cloudcmd.main,

        DIR                 = main.DIR,
        LIBDIR              = main.LIBDIR,
        SRVDIR              = main.SRVDIR,

        URL                 = main.url,
        Path                = main.path,
        Querystring         = main.querystring,

        Minify              = main.minify,
        CloudFunc           = main.cloudfunc,
        AppCache            = main.appcache,
        Socket              = main.socket,
        Console             = main.console,
        Terminal            = main.terminal,

        zlib                = main.zlib,
        http                = main.http,
        https               = main.https,
        Util                = main.util,
        express             = main.express,
        expressApp,
        files               = main.files,

        Server, Rest, Route;

    /* базовая инициализация  */
    function init(pAppCachProcessing) {
        var config = main.config;

        /* создаём файл app cache */
        if (config.appcache  && AppCache && config.server )
            Util.exec( pAppCachProcessing );
    }

    /**
     * start server function
     * @param pConfig
     * @param pProcessing {index, appcache, rest}
     */
    function start(options) {
        var redirectServer,
            config         = main.config;

        if (!options)
            options = {};

        Rest            =   options.rest;
        Route           =   options.route;

        init(options.appcache);

        var lPort       =   process.env.PORT            ||  /* c9           */
                            process.env.app_port        ||  /* nodester     */
                            process.env.VCAP_APP_PORT   ||  /* cloudfoundry */
                            config.port,

            lIP         =   process.env.IP              ||  /* c9           */
                            config.ip                   ||
                            (main.WIN32 ? '127.0.0.1' : '0.0.0.0'),

            lSSL        = options.ssl,
            lSSLPort    = config.sslPort,
            lHTTP       = 'http://',
            lHTTPS      = 'https://',

            lSockets    = function(pServer) {
                var listen, msg,
                    status = 'off';

                if (config.socket && Socket) {
                    listen = Socket.listen(pServer);

                    if (listen) {
                        status = 'on';
                        Console.init();
                        Terminal.init();
                    }
                }

                msg     = CloudFunc.formatMsg('sockets', '', status);

                Util.log(msg);
            },

            lHTTPServer = function() {
                expressApp  = express.getApp([
                    Rest,
                    Route,
                    join,
                    controller
                ]);

                Server      = http.createServer(expressApp || respond);

                Server.on('error', Util.log.bind(Util));
                Server.listen(lPort, lIP);
                lServerLog(lHTTP, lPort);
                lSockets(Server);
            },

            lServerLog  = function(http, port) {
                Util.log('* Server running at ' + http + lIP + ':' + port);
            };
        /* server mode or testing mode */
        if (config.server)
            if (lSSL) {
                Util.log('* Redirection http -> https is setted up');
                lServerLog(lHTTP, lPort);

                redirectServer =  http.createServer(function(req, res) {
                    var url,
                        host        = req.headers.host,
                        parsed      = url.parse(lHost),
                        hostName    = parsed.protocol;

                    url             = lHTTPS + hostName + lSSLPort + req.url;

                    main.redirect({
                        response: res,
                        url: url
                    });
                });

                redirectServer.listen(lPort, lIP);

                Server  =  https.createServer(lSSL, respond);
                Server.on('error', function (error) {
                    Util.log('Could not use https port: ' + lSSLPort);
                    Util.log(error);
                    redirectServer.close();
                    Util.log('* Redirection http -> https removed');
                    lHTTPServer();
                });

                lSockets(Server);

                Server.listen(lSSLPort, lIP);
                lServerLog(lHTTPS, lSSLPort);
            } else
                lHTTPServer();
        else
            Util.log('Cloud Commander testing mode');
    }

    function respond(req, res) {
        var i, n, func,
            funcs = ([
                Rest,
                Route,
                join,
                controller
            ]);

        n = funcs.length;
        for (i = 0; i < n; i++) {
            func        = funcs[i];
            funcs[i]    = func.bind(null, req, res);
        }

        Util.loadOnLoad(funcs);
    }

    /**
     * Главная функция, через которую проихсодит
     * взаимодействие, обмен данными с клиентом
     * @param req - запрос клиента (Request)
     * @param res - ответ сервера (Response)
     */
    function controller(req, res) {
        var check, result,
            config      = main.config,
            isMin       = config.minify,
            parsedUrl   = URL.parse(req.url),
            query       = parsedUrl.search || '',
            path        = main.getPathName(req),
            name        = path;

        if (!expressApp)
            Util.log(req.method + ' ' + path + query);

        /* watching is file changed */
        if (config.appcache)
            AppCache.watch(name);

        name    = Path.join(DIR, name);
        check   = checkExtension(name);
        result  = isMin && check;

        Util.ifExec(!result,
            function(params) {
                var sendName = params && params.name || name;

                main.sendFile({
                    name        : sendName,
                    cache       : config.cache,
                    gzip        : true,
                    request     : req,
                    response    : res
                });
        }, function(callback) {
            Minify.optimize(name, {
                callback    : callback,
                returnName  : true
            });
        });
    }

    function minify(name) {
        return function(callback) {
            Minify.optimize(name, {
                callback    : callback
            });
        };
    }

    function join(request, response, callback) {
        var names, i, n, name, minName, stream, check,
            funcs       = [],
            config      = main.config,
            dir         = DIR,
            gzip        = zlib.createGzip(),
            isGzip      = main.isGZIP(request),
            path        = main.getPathName(request),

            isJoin      = CloudFunc.isJoinURL(path),
            readPipe    = function() {
                main.mainSetHeader({
                    name        : names[0],
                    cache       : config.cache,
                    gzip        : isGzip,
                    request     : request,
                    response    : response
                });

                if (!isGzip)
                    stream = response;
                else
                    stream = gzip;

                files.readPipe({
                    names       : names,
                    dir         : dir,
                    write       : stream,
                    callback    : function(error) {
                        var errorStr;

                        if (error)
                            if (!response.headersSent)
                                main.sendError({
                                    request     : request,
                                    response    : response,
                                    name        : path
                                }, error);
                            else {
                                Util.log(error);
                                errorStr = error.toString();
                                stream.end(errorStr);
                            }
                    }
                });

                /*
                 * pipe should be setted up after
                 * readPipe called with stream param
                 */
                if (isGzip)
                    gzip.pipe(response);
            };

        if (isJoin) {
            names   = CloudFunc.getJoinArray(path);
            n       = names.length;

            if (!config.minify)
                readPipe();
            else {
                for (i = 0; i < n; i++) {
                    name        = Path.join(DIR, names[i]);
                    check       = checkExtension(name);

                    if (check) {
                        minName = Minify.getName(name);

                        if (name !== minName) {
                            names[i] = minName;
                            dir = '';
                        }
                    }

                    funcs.push(minify(name));
                }
                Util.asyncCall(funcs, readPipe);
            }
        } else
            Util.exec(callback);

        return isJoin;
    }

    function checkExtension(name) {
        var ret;

        ret = Util.checkExtension(name, ['.js', '.css', '.html']);

        return ret;
    }

    exports.start           = start;

})();
<|endoftext|>


<commit_before>
/**
 * Module dependencies.
 */

var assert = require('assert');
var inherits = require('util').inherits;
var Transform = require('stream').Transform;
var debug = require('debug')('stream:parser');

// for node v0.8.x support, remove after v0.12.x
if (!Transform) Transform = require('readable-stream/transform');

/**
 * Module exports.
 */

module.exports = Parser;

/**
 * Abstraction on top of the `Transform` base Stream class. Offers:
 *
 *   buffer(n, cb) - buffers "n" bytes and then calls "cb" with the "chunk"
 *   passthrough(n, cb) - passes through "n" bytes untouched and then calls "cb"
 */

function Parser (opts) {
  Transform.call(this, opts);
  this._left = 0;
  this._buffers = [];
  this._buffered = 0;
  this._buffering = false;
}
inherits(Parser, Transform);

/**
 * Buffers "n" bytes and then invokes "cb" once that amount is satisfied.
 */

Parser.prototype.buffer = function (n, cb) {
  assert(!this._cb, 'There is already a "callback" set!');
  assert(isFinite(n), 'Can only buffer a finite number of bytes, got "' + n + '"');
  debug('buffering "%d" bytes', n);
  this._left = n;
  this._cb = cb;
  this._buffering = true;
};

/**
 * Passes through "n" bytes to the readable side of this stream, then invokes "cb"
 * once that amount is satisfied.
 */

Parser.prototype.passthrough = function (n, cb) {
  assert(!this._cb, 'There is already a "callback" set!');
  debug('passing through "%d" bytes', n);
  this._left = n;
  this._cb = cb;
  this._buffering = false;
};

/**
 * The internal buffering/passthrough logic...
 */

Parser.prototype._transform = function (chunk, write, done) {
  debug('_transform: (chunk.length: %d)', chunk.length);
  if (this.done) {
    debug('_transform called, but stream is "done"!');
    return done();
  }
  assert(this._left > 0);
  if (chunk.length <= this._left) {
    // small buffer fits within the "_left" window
    this._onData(chunk, write);
    done();
  } else {
    // large buffer needs to be sliced on "_left" and processed
    var b = chunk.slice(0, this._left);
    this._onData(b, write);
    if (chunk.length > b.length) {
      this._transform(chunk.slice(b.length), write, done);
    }
  }
};

Parser.prototype._onData = function (chunk, write) {
  this._left -= chunk.length;
  if (this._buffering) {
    // buffer
    this._buffers.push(chunk);
    this._buffered += chunk.length;
  } else {
    // passthrough
    write(chunk);
  }

  if (0 === this._left) {
    // done with this "piece", invoke the callback
    var cb = this._cb;
    if (cb && this._buffering && this._buffers.length > 1) {
      chunk = Buffer.concat(this._buffers, this._buffered);
    }
    if (!this._buffering) {
      chunk = null;
    }
    this._cb = null;
    this._buffered = 0;
    this._buffering = true;
    this._buffers.splice(0); // empty
    if (cb) {
      if (chunk) {
        cb.call(this, chunk, write); // buffered
      } else {
        cb.call(this, write); // passthrough
      }
    }
    assert(this.done || this._left > 0, 'buffer() or passthrough() were not called in the callback');
  }
};
<commit_msg>delete parser.js<commit_after><|endoftext|>


<commit_before>/*
 * Copyright (c) 2013-2015 node-coap contributors.
 *
 * node-coap is licensed under an MIT +no-false-attribs license.
 * All rights not explicitly granted in the MIT license are reserved.
 * See the included LICENSE file for more details.
 */

require('./polyfill')

var dgram           = require('dgram')
  , os              = require('os')
  , net             = require('net')
  , util            = require('util')
  , series          = require('fastseries')
  , crypto          = require('crypto')
  , events          = require('events')
  , LRU             = require('lru-cache')
  , parse           = require('coap-packet').parse
  , generate        = require('coap-packet').generate
  , IncomingMessage = require('./incoming_message')
  , OutgoingMessage = require('./outgoing_message')
  , ObserveStream   = require('./observe_write_stream')
  , parameters      = require('./parameters')
  , RetrySend       = require('./retry_send')
  , parseBlock2     = require('./helpers').parseBlock2
  , createBlock2    = require('./helpers').createBlock2
  , getOption       = require('./helpers').getOption
  , isNumeric       = require('./helpers').isNumeric
  , isBoolean       = require('./helpers').isBoolean
  , middlewares     = require('./middlewares')
  , debug           = require('debug')('CoAP Server')

function handleEnding(err) {
  var request = this
  if (err) {
    request.server._sendError(new Buffer(err.message), request.rsinfo, request.packet)
  }
}

function CoAPServer(options, listener) {
  if (!(this instanceof CoAPServer)) {
    return new CoAPServer(options, listener)
  }

  if (typeof options === 'function') {
    listener = options
    options = null
  }

  if (!options)
    options = {}

  this._options = options
  this._proxiedRequests = {}

  this._middlewares = [
    middlewares.parseRequest
  ]

  if (options.proxy) {
    this._middlewares.push(middlewares.proxyRequest)
    this._middlewares.push(middlewares.handleProxyResponse)
  }

  if (!this._options.piggybackReplyMs || !isNumeric(this._options.piggybackReplyMs)) {
    this._options.piggybackReplyMs = parameters.piggybackReplyMs
  }

  if (!isBoolean(this._options.sendAcksForNonConfirmablePackets)) {
    this._options.sendAcksForNonConfirmablePackets = parameters.sendAcksForNonConfirmablePackets
  }
  this._middlewares.push(middlewares.handleServerRequest)

  // Multicast settings
  this._multicastAddress = options.multicastAddress ? options.multicastAddress : null
  this._multicastInterface = options.multicastInterface ? options.multicastInterface : null

  // We use an LRU cache for the responses to avoid
  // DDOS problems.
  // max packet size is 1280
  // 32 MB / 1280 = 26214
  // The max lifetime is roughly 200s per packet.
  // Which gave us 131 packets/second guarantee
  this._lru = LRU({
      max: options.cacheSize || (32768 * 1024)
    , length: function(n) { return n.buffer.byteLength }
    , maxAge: (parameters.exchangeLifetime * 1000)
    , dispose:  function(key, value) {
                  if (value.sender)
                    value.sender.reset()
                }
  })

  this._series = series()
  this._block2Cache = {}

  if (listener)
    this.on('request', listener)
  debug('initialized');
}

util.inherits(CoAPServer, events.EventEmitter)

CoAPServer.prototype._sendError = function(payload, rsinfo, packet) {
  var message = generate({
    code: '5.00',
    payload: payload,
    messageId: (packet)?packet.messageId:undefined,
    token: (packet)?packet.token:undefined
  })

  this._sock.send(message, 0, message.length, rsinfo.port)
}

function removeProxyOptions(packet) {
  var cleanOptions = []

  for (var i = 0; i < packet.options.length; i++) {
    if (packet.options[i].name.toLowerCase() !== 'proxy-uri' && packet.options[i].name.toLowerCase() !== 'proxy-scheme') {
      cleanOptions.push(packet.options[i])
    }
  }

  packet.options = cleanOptions

  return packet;
}

CoAPServer.prototype._sendProxied = function(packet, proxyUri, callback) {
  var url = require('url').parse(proxyUri)
    , host = url.hostname
    , port = url.port
    , message = generate(removeProxyOptions(packet))

  this._sock.send(message, 0, message.length, port, host, callback)
}

CoAPServer.prototype._sendReverseProxied = function(packet, rsinfo, callback) {
  var host = rsinfo.address
    , port = rsinfo.port
    , message = generate(packet)

  this._sock.send(message, 0, message.length, port, host, callback)
}

function handleRequest(server) {
  return function (msg, rsinfo) {
    var request = {
        raw: msg,
        rsinfo: rsinfo,
        server: server
      }
      , activeMiddlewares = []

    for (var i = 0; i < server._middlewares.length; i++) {
      activeMiddlewares.push(server._middlewares[i])
    }

    server._series(request, activeMiddlewares, request, handleEnding)
  }
}

function allAddresses(type) {
    var family = 'IPv4'
    if (type === 'udp6') {
        family = 'IPv6'
    }
    var addresses = [];
    var interfaces = os.networkInterfaces();
    for (var ifname in interfaces)  {
        if (interfaces.hasOwnProperty(ifname)) {
            interfaces[ifname].forEach(function (a) {
                if (a.family == family) {
                    addresses.push(a.address)
                }
            })
        }
    }
    return addresses;
}

CoAPServer.prototype.listen = function(port, address, done) {
  var that = this

  if (port == undefined) {
    port = parameters.coapPort
  }

  if (typeof port === 'function') {
    done = port
    port = parameters.coapPort
  }

  if (typeof address === 'function') {
    done = address
    address = null
  }

  if (this._sock) {
    if (done)
      done(new Error('Already listening'))
    else
      throw new Error('Already listening')

    return this
  }

  if (address && net.isIPv6(address))
    this._options.type = 'udp6'

  if (!this._options.type)
    this._options.type = 'udp4'

  this._sock = dgram.createSocket({type: this._options.type, reuseAddr : true}, handleRequest(this))

  this._sock.on('error', function(error) {
    that.emit('error', error)
  })

  this._sock.bind(port, address || null, function () {
    if (that._multicastAddress) {
      that._sock.setMulticastLoopback(true)

      if (that._multicastInterface) {
        that._sock.addMembership(that._multicastAddress, that._multicastInterface)
      } else {
        allAddresses(that._options.type).forEach(function(interface) {
            that._sock.addMembership(that._multicastAddress, interface)
        })
      }
    }
    if (done) done()
  })
  this._port = port
  this._address = address

  return this
}

CoAPServer.prototype.close = function(done) {
  if (done) {
    setImmediate(done)
  }

  if (this._sock) {
    this._sock.close()
    this._lru.reset()
    this._sock = null
    this.emit('close')
  } else {
    this._lru.reset()
  }

  // cancel cache entry expiry timers
  for (var k in this._block2Cache) {
    if (this._block2Cache.hasOwnProperty(k)) {
      debug('clean-up cache expiry timer, key:', k)
      clearTimeout(this._block2Cache[k].timeoutId)
      delete this._block2Cache[k]
    }
  }

  return this
}

function expiry(block2cache, k) {
  debug('delete expired cache entry, key:', k)
  delete block2cache[k]
}

CoAPServer.prototype._handle = function(packet, rsinfo) {

  var sock      = this._sock
    , lru       = this._lru
    , acks      = this._acks
    , cached    = lru.peek(toKey(rsinfo.address, rsinfo.port, packet, true))
    , Message   = OutMessage
    , that = this
    , request
    , response

  if (cached && !packet.ack && !packet.reset) {
    return sock.send(cached, 0, cached.length, rsinfo.port, rsinfo.address)
  } else if (cached && (packet.ack || packet.reset)) {
    if (cached.response && packet.reset) {
      cached.response.end()
    }
    return lru.del(toKey(rsinfo.address, rsinfo.port, packet, false))
  }
  else if (packet.ack || packet.reset) {
    return // nothing to do, ignoring silently
  }

  request = new IncomingMessage(packet, rsinfo)

  if (request.headers['Observe'] === 0) {
    Message = ObserveStream
    if (packet.code !== '0.01')
      // it is not a GET
      return this._sendError(new Buffer('Observe can only be present with a GET'), rsinfo)
  }

  packet.piggybackReplyMs = this._options.piggybackReplyMs;
  response = new Message(packet, function(response, packet) {
    var buf
      , sender = new RetrySend(sock, rsinfo.port, rsinfo.address)

    try {
      buf = generate(packet)
    } catch(err) {
      return response.emit('error', err)
    }

    if (Message === OutMessage) {
      sender.on('error', response.emit.bind(response, 'error'))
    } else {
      buf.response = response
      sender.on('error', function() {
        response.end()
      })
    }

    lru.set(toKey(rsinfo.address, rsinfo.port,
                  packet, packet.ack || !packet.confirmable), buf)
    buf.sender = sender

    if (that._options.sendAcksForNonConfirmablePackets || packet.confirmable){
      sender.send(buf, packet.ack || packet.reset || packet.confirmable === false)
    } else {
      debug('OMIT ACK PACKAGE')
    }
  })

  request.rsinfo = rsinfo
  response.statusCode = '2.05'
  response._request = request._packet
  response._cachekey = toCacheKey(rsinfo.address, rsinfo.port, packet)

  var block2cache = this._block2Cache
  //inject this function so the response can add an entry to the cache
  response._addCacheEntry = function(key, payload) {
    if (block2cache.hasOwnProperty(key)) {
      debug('reuse old cache entry, key:', key)
      clearTimeout(block2cache[key].timeoutId) // cancel old expiry timer
      block2cache[key].payload = payload
    } else {
      debug('add payload to cache, key:', key)
      block2cache[key] = {payload: payload}
    }
    // setup new expiry timer
    block2cache[key].timeoutId = setTimeout(expiry, parameters.exchangeLifetime * 1000, block2cache, key)
  }

  //return cached value for blockwise requests
  var cachedResponseSend = false
  if (packet.token && packet.token.length > 0) {
    // return cached value only if this request is not the first block request
    var block2Buff = getOption(response._request.options, 'Block2')
    var requestedBlockOption
    if (block2Buff) {
      requestedBlockOption = parseBlock2(block2Buff)
    }
    if (!requestedBlockOption) {
      requestedBlockOption = {num: 0}
    }

    if (requestedBlockOption.num < 1) {
      if (this._block2Cache.hasOwnProperty(response._cachekey)) {
        debug('first block2 request, remove old entry from cache, key:', response._cachekey)
        clearTimeout(this._block2Cache[response._cachekey].timeoutId)
        delete this._block2Cache[response._cachekey]
      }
    } else {
      debug('check if packet token is in cache, key:', response._cachekey)
      if (this._block2Cache.hasOwnProperty(response._cachekey)) {
        debug('found cached payload, key:', response._cachekey)
        response.end(this._block2Cache[response._cachekey].payload)
        cachedResponseSend = true
      }
    }
  }

  if (!cachedResponseSend) {
    debug('no cached entry found, emit request to upper layer');
    this.emit('request', request, response)
  }
}

function toCacheKey(address, port, packet) {
  if (packet.token && packet.token.length > 0) {
    return packet.token.toString('hex') + '/' + address + ':' + port
  }

  return null
}

function toKey(address, port, packet, appendToken) {
  var result = address + port + packet.messageId

  if (appendToken)
    result += packet.token.toString('hex')

  return result
}

/*
new out message
inherit from OutgoingMessage
to handle cached answer and blockwise (2)
*/
function OutMessage() {
  OutgoingMessage.apply(this, Array.prototype.slice.call(arguments));
}
util.inherits(OutMessage, OutgoingMessage)

// maxBlock2 is in formular 2**(i+4), and must <= 2**(6+4)
var maxBlock2 = Math.pow(2, Math.floor(Math.log(parameters.maxPacketSize)/Math.log(2)))
if (maxBlock2 > Math.pow(2, (6+4)))
  maxBlock2 = Math.pow(2, (6+4))

OutMessage.prototype.end= function(payload) {
  var that = this

  var block2Buff = getOption(this._request.options, 'Block2')
  var requestedBlockOption
  // if we got blockwise (2) resquest
  if (block2Buff) {
    requestedBlockOption = parseBlock2(block2Buff)
    // bad option
    if (!requestedBlockOption) {
      that.statusCode = '4.02'
      return OutgoingMessage.prototype.end.call(that)
    }
  }

  // if payload is suitable for ONE message, shoot it out
  if (!payload ||
    ((!requestedBlockOption) && (payload.length < parameters.maxPacketSize)))
    return OutgoingMessage.prototype.end.call(this, payload)

  // for the first request, block2 option may be missed
  if (!requestedBlockOption)
    requestedBlockOption = {
      size: maxBlock2,
      num: 0
    }

  // block2 size should not bigger than maxBlock2
  if (requestedBlockOption.size > maxBlock2)
    requestedBlockOption.size = maxBlock2

  // block number should have limit
  // 0 base counter for totalBlock, hence use floor (vs ceil)
  var totalBlock = Math.floor(payload.length/requestedBlockOption.size)
  var isLastBlock
  if (requestedBlockOption.num < totalBlock)
    isLastBlock = false
  else if (requestedBlockOption.num == totalBlock)
    isLastBlock = true
  else {
    // precondition fail, may request for out of range block
    that.statusCode = '4.02'
    return OutgoingMessage.prototype.end.call(that)
  }

  var block2 = createBlock2({
    moreBlock2: isLastBlock,
    num: requestedBlockOption.num,
    size: requestedBlockOption.size
  })
  if (!block2) {
    // this catch never be match,
    // since we're gentleman, just handle it
    that.statusCode = '4.02'
    return OutgoingMessage.prototype.end.call(that)
  }
  this.setOption('Block2', block2)
  this.setOption('ETag', _toETag(payload))

  // cache it
  if (this._request.token && this._request.token.length > 0) {
    this._addCacheEntry(this._cachekey, payload)
  }
  OutgoingMessage.prototype.end.call(this, payload.slice((requestedBlockOption.num)*requestedBlockOption.size, (requestedBlockOption.num+1)*requestedBlockOption.size))
};

/*
calculate id of a payload by xor each 2-byte-block from it
use to generate etag
  payload         an input buffer, represent payload need to generate id (hash)
  id              return var, is a buffer(2)
*/
function _toETag(payload) {
  var id = new Buffer([0,0])
  var i = 0
  do {
    id[0] ^= payload[i]
    id[1] ^= payload[i+1]
    i += 2
  } while (i<payload.length)
  return id
}

module.exports = CoAPServer
<commit_msg>Add timer to actively prune buf from lru<commit_after>/*
 * Copyright (c) 2013-2015 node-coap contributors.
 *
 * node-coap is licensed under an MIT +no-false-attribs license.
 * All rights not explicitly granted in the MIT license are reserved.
 * See the included LICENSE file for more details.
 */

require('./polyfill')

var dgram           = require('dgram')
  , os              = require('os')
  , net             = require('net')
  , util            = require('util')
  , series          = require('fastseries')
  , crypto          = require('crypto')
  , events          = require('events')
  , LRU             = require('lru-cache')
  , parse           = require('coap-packet').parse
  , generate        = require('coap-packet').generate
  , IncomingMessage = require('./incoming_message')
  , OutgoingMessage = require('./outgoing_message')
  , ObserveStream   = require('./observe_write_stream')
  , parameters      = require('./parameters')
  , RetrySend       = require('./retry_send')
  , parseBlock2     = require('./helpers').parseBlock2
  , createBlock2    = require('./helpers').createBlock2
  , getOption       = require('./helpers').getOption
  , isNumeric       = require('./helpers').isNumeric
  , isBoolean       = require('./helpers').isBoolean
  , middlewares     = require('./middlewares')
  , debug           = require('debug')('CoAP Server')

function handleEnding(err) {
  var request = this
  if (err) {
    request.server._sendError(new Buffer(err.message), request.rsinfo, request.packet)
  }
}

function CoAPServer(options, listener) {
  if (!(this instanceof CoAPServer)) {
    return new CoAPServer(options, listener)
  }

  if (typeof options === 'function') {
    listener = options
    options = null
  }

  if (!options)
    options = {}

  this._options = options
  this._proxiedRequests = {}

  this._middlewares = [
    middlewares.parseRequest
  ]

  if (options.proxy) {
    this._middlewares.push(middlewares.proxyRequest)
    this._middlewares.push(middlewares.handleProxyResponse)
  }

  if (!this._options.piggybackReplyMs || !isNumeric(this._options.piggybackReplyMs)) {
    this._options.piggybackReplyMs = parameters.piggybackReplyMs
  }

  if (!isBoolean(this._options.sendAcksForNonConfirmablePackets)) {
    this._options.sendAcksForNonConfirmablePackets = parameters.sendAcksForNonConfirmablePackets
  }
  this._middlewares.push(middlewares.handleServerRequest)

  // Multicast settings
  this._multicastAddress = options.multicastAddress ? options.multicastAddress : null
  this._multicastInterface = options.multicastInterface ? options.multicastInterface : null

  // We use an LRU cache for the responses to avoid
  // DDOS problems.
  // max packet size is 1280
  // 32 MB / 1280 = 26214
  // The max lifetime is roughly 200s per packet.
  // Which gave us 131 packets/second guarantee
  this._lruMaxAge = (parameters.exchangeLifetime * 1000)
  this._lru = LRU({
      max: options.cacheSize || (32768 * 1024)
    , length: function(n) { return n.buffer.byteLength }
    , maxAge: this._lruMaxAge
    , dispose:  function(key, value) {
                  if (value.sender)
                    value.sender.reset()
                }
  })

  this._series = series()
  this._block2Cache = {}

  if (listener)
    this.on('request', listener)
  debug('initialized');
}

util.inherits(CoAPServer, events.EventEmitter)

CoAPServer.prototype._sendError = function(payload, rsinfo, packet) {
  var message = generate({
    code: '5.00',
    payload: payload,
    messageId: (packet)?packet.messageId:undefined,
    token: (packet)?packet.token:undefined
  })

  this._sock.send(message, 0, message.length, rsinfo.port)
}

function removeProxyOptions(packet) {
  var cleanOptions = []

  for (var i = 0; i < packet.options.length; i++) {
    if (packet.options[i].name.toLowerCase() !== 'proxy-uri' && packet.options[i].name.toLowerCase() !== 'proxy-scheme') {
      cleanOptions.push(packet.options[i])
    }
  }

  packet.options = cleanOptions

  return packet;
}

CoAPServer.prototype._sendProxied = function(packet, proxyUri, callback) {
  var url = require('url').parse(proxyUri)
    , host = url.hostname
    , port = url.port
    , message = generate(removeProxyOptions(packet))

  this._sock.send(message, 0, message.length, port, host, callback)
}

CoAPServer.prototype._sendReverseProxied = function(packet, rsinfo, callback) {
  var host = rsinfo.address
    , port = rsinfo.port
    , message = generate(packet)

  this._sock.send(message, 0, message.length, port, host, callback)
}

function handleRequest(server) {
  return function (msg, rsinfo) {
    var request = {
        raw: msg,
        rsinfo: rsinfo,
        server: server
      }
      , activeMiddlewares = []

    for (var i = 0; i < server._middlewares.length; i++) {
      activeMiddlewares.push(server._middlewares[i])
    }

    server._series(request, activeMiddlewares, request, handleEnding)
  }
}

function allAddresses(type) {
    var family = 'IPv4'
    if (type === 'udp6') {
        family = 'IPv6'
    }
    var addresses = [];
    var interfaces = os.networkInterfaces();
    for (var ifname in interfaces)  {
        if (interfaces.hasOwnProperty(ifname)) {
            interfaces[ifname].forEach(function (a) {
                if (a.family == family) {
                    addresses.push(a.address)
                }
            })
        }
    }
    return addresses;
}

CoAPServer.prototype.listen = function(port, address, done) {
  var that = this

  if (port == undefined) {
    port = parameters.coapPort
  }

  if (typeof port === 'function') {
    done = port
    port = parameters.coapPort
  }

  if (typeof address === 'function') {
    done = address
    address = null
  }

  if (this._sock) {
    if (done)
      done(new Error('Already listening'))
    else
      throw new Error('Already listening')

    return this
  }

  if (address && net.isIPv6(address))
    this._options.type = 'udp6'

  if (!this._options.type)
    this._options.type = 'udp4'

  this._sock = dgram.createSocket({type: this._options.type, reuseAddr : true}, handleRequest(this))

  this._sock.on('error', function(error) {
    that.emit('error', error)
  })

  this._sock.bind(port, address || null, function () {
    if (that._multicastAddress) {
      that._sock.setMulticastLoopback(true)

      if (that._multicastInterface) {
        that._sock.addMembership(that._multicastAddress, that._multicastInterface)
      } else {
        allAddresses(that._options.type).forEach(function(interface) {
            that._sock.addMembership(that._multicastAddress, interface)
        })
      }
    }
    if (done) done()
  })
  this._port = port
  this._address = address

  return this
}

CoAPServer.prototype.close = function(done) {
  if (done) {
    setImmediate(done)
  }

  if (this._sock) {
    this._sock.close()

    this._lru.reset()
    this._sock = null
    this.emit('close')
  } else {
    this._lru.reset()
  }

  // cancel cache entry expiry timers
  for (var k in this._block2Cache) {
    if (this._block2Cache.hasOwnProperty(k)) {
      debug('clean-up cache expiry timer, key:', k)
      clearTimeout(this._block2Cache[k].timeoutId)
      delete this._block2Cache[k]
    }
  }

  return this
}

function expiry(block2cache, k) {
  debug('delete expired cache entry, key:', k)
  delete block2cache[k]
}

CoAPServer.prototype._handle = function(packet, rsinfo) {

  var sock      = this._sock
    , lru       = this._lru
    , acks      = this._acks
    , cached    = lru.peek(toKey(rsinfo.address, rsinfo.port, packet, true))
    , Message   = OutMessage
    , that = this
    , request
    , response

  if (cached && !packet.ack && !packet.reset) {
    return sock.send(cached, 0, cached.length, rsinfo.port, rsinfo.address)
  } else if (cached && (packet.ack || packet.reset)) {
    if (cached.response && packet.reset) {
      cached.response.end()
    }
    return lru.del(toKey(rsinfo.address, rsinfo.port, packet, false))
  }
  else if (packet.ack || packet.reset) {
    return // nothing to do, ignoring silently
  }

  request = new IncomingMessage(packet, rsinfo)

  if (request.headers['Observe'] === 0) {
    Message = ObserveStream
    if (packet.code !== '0.01')
      // it is not a GET
      return this._sendError(new Buffer('Observe can only be present with a GET'), rsinfo)
  }

  packet.piggybackReplyMs = this._options.piggybackReplyMs;
  response = new Message(packet, function(response, packet) {
    var buf
      , sender = new RetrySend(sock, rsinfo.port, rsinfo.address)

    try {
      buf = generate(packet)
    } catch(err) {
      return response.emit('error', err)
    }

    if (Message === OutMessage) {
      sender.on('error', response.emit.bind(response, 'error'))
    } else {
      buf.response = response
      sender.on('error', function() {
        response.end()
      })
    }

    var key = toKey(rsinfo.address, rsinfo.port,
        packet, packet.ack || !packet.confirmable)
    lru.set(key, buf)

    buf.sender = sender

    // Start timer to actively prune from LRU cache after expiration
    buf.lru_timer = setTimeout(function () {
      lru.get(key);
    }, that._lruMaxAge + 1000)
    buf.lru_timer.unref()

    if (that._options.sendAcksForNonConfirmablePackets || packet.confirmable){
      sender.send(buf, packet.ack || packet.reset || packet.confirmable === false)
    } else {
      debug('OMIT ACK PACKAGE')
    }
  })

  request.rsinfo = rsinfo
  response.statusCode = '2.05'
  response._request = request._packet
  response._cachekey = toCacheKey(rsinfo.address, rsinfo.port, packet)

  var block2cache = this._block2Cache
  //inject this function so the response can add an entry to the cache
  response._addCacheEntry = function(key, payload) {
    if (block2cache.hasOwnProperty(key)) {
      debug('reuse old cache entry, key:', key)
      clearTimeout(block2cache[key].timeoutId) // cancel old expiry timer
      block2cache[key].payload = payload
    } else {
      debug('add payload to cache, key:', key)
      block2cache[key] = {payload: payload}
    }
    // setup new expiry timer
    block2cache[key].timeoutId = setTimeout(expiry, parameters.exchangeLifetime * 1000, block2cache, key)
  }

  //return cached value for blockwise requests
  var cachedResponseSend = false
  if (packet.token && packet.token.length > 0) {
    // return cached value only if this request is not the first block request
    var block2Buff = getOption(response._request.options, 'Block2')
    var requestedBlockOption
    if (block2Buff) {
      requestedBlockOption = parseBlock2(block2Buff)
    }
    if (!requestedBlockOption) {
      requestedBlockOption = {num: 0}
    }

    if (requestedBlockOption.num < 1) {
      if (this._block2Cache.hasOwnProperty(response._cachekey)) {
        debug('first block2 request, remove old entry from cache, key:', response._cachekey)
        clearTimeout(this._block2Cache[response._cachekey].timeoutId)
        delete this._block2Cache[response._cachekey]
      }
    } else {
      debug('check if packet token is in cache, key:', response._cachekey)
      if (this._block2Cache.hasOwnProperty(response._cachekey)) {
        debug('found cached payload, key:', response._cachekey)
        response.end(this._block2Cache[response._cachekey].payload)
        cachedResponseSend = true
      }
    }
  }

  if (!cachedResponseSend) {
    debug('no cached entry found, emit request to upper layer');
    this.emit('request', request, response)
  }
}

function toCacheKey(address, port, packet) {
  if (packet.token && packet.token.length > 0) {
    return packet.token.toString('hex') + '/' + address + ':' + port
  }

  return null
}

function toKey(address, port, packet, appendToken) {
  var result = address + port + packet.messageId

  if (appendToken)
    result += packet.token.toString('hex')

  return result
}

/*
new out message
inherit from OutgoingMessage
to handle cached answer and blockwise (2)
*/
function OutMessage() {
  OutgoingMessage.apply(this, Array.prototype.slice.call(arguments));
}
util.inherits(OutMessage, OutgoingMessage)

// maxBlock2 is in formular 2**(i+4), and must <= 2**(6+4)
var maxBlock2 = Math.pow(2, Math.floor(Math.log(parameters.maxPacketSize)/Math.log(2)))
if (maxBlock2 > Math.pow(2, (6+4)))
  maxBlock2 = Math.pow(2, (6+4))

OutMessage.prototype.end= function(payload) {
  var that = this

  var block2Buff = getOption(this._request.options, 'Block2')
  var requestedBlockOption
  // if we got blockwise (2) resquest
  if (block2Buff) {
    requestedBlockOption = parseBlock2(block2Buff)
    // bad option
    if (!requestedBlockOption) {
      that.statusCode = '4.02'
      return OutgoingMessage.prototype.end.call(that)
    }
  }

  // if payload is suitable for ONE message, shoot it out
  if (!payload ||
    ((!requestedBlockOption) && (payload.length < parameters.maxPacketSize)))
    return OutgoingMessage.prototype.end.call(this, payload)

  // for the first request, block2 option may be missed
  if (!requestedBlockOption)
    requestedBlockOption = {
      size: maxBlock2,
      num: 0
    }

  // block2 size should not bigger than maxBlock2
  if (requestedBlockOption.size > maxBlock2)
    requestedBlockOption.size = maxBlock2

  // block number should have limit
  // 0 base counter for totalBlock, hence use floor (vs ceil)
  var totalBlock = Math.floor(payload.length/requestedBlockOption.size)
  var isLastBlock
  if (requestedBlockOption.num < totalBlock)
    isLastBlock = false
  else if (requestedBlockOption.num == totalBlock)
    isLastBlock = true
  else {
    // precondition fail, may request for out of range block
    that.statusCode = '4.02'
    return OutgoingMessage.prototype.end.call(that)
  }

  var block2 = createBlock2({
    moreBlock2: isLastBlock,
    num: requestedBlockOption.num,
    size: requestedBlockOption.size
  })
  if (!block2) {
    // this catch never be match,
    // since we're gentleman, just handle it
    that.statusCode = '4.02'
    return OutgoingMessage.prototype.end.call(that)
  }
  this.setOption('Block2', block2)
  this.setOption('ETag', _toETag(payload))

  // cache it
  if (this._request.token && this._request.token.length > 0) {
    this._addCacheEntry(this._cachekey, payload)
  }
  OutgoingMessage.prototype.end.call(this, payload.slice((requestedBlockOption.num)*requestedBlockOption.size, (requestedBlockOption.num+1)*requestedBlockOption.size))
};

/*
calculate id of a payload by xor each 2-byte-block from it
use to generate etag
  payload         an input buffer, represent payload need to generate id (hash)
  id              return var, is a buffer(2)
*/
function _toETag(payload) {
  var id = new Buffer([0,0])
  var i = 0
  do {
    id[0] ^= payload[i]
    id[1] ^= payload[i+1]
    i += 2
  } while (i<payload.length)
  return id
}

module.exports = CoAPServer
<|endoftext|>


<commit_before>'use strict';

/**
 * Native modules.
 */
var fs = require('fs')
  , path = require('path');

/**
 * Required defaults.
 */
var defaults = require('./config')
  , sections = defaults.sections
  , names = Object.keys(sections)
  , maps = defaults.keys
  , parser = module.exports
  , keys = {}
  , config;

/**
 * Config storage.
 */
parser.config = config = {};

/**
 * Composer functions.
 */
var compose = {
    /**
     * Composer function for JSON, strips comments and utilizes stringify.
     *
     * @param {Object} data
     * @returns {String} stringified data.
     * @api private
     */
    json: function stringify(data) {
      var clone = JSON.parse(JSON.stringify(data));

      // Remove commentary as it has no place in JSON.
      Object.keys(clone).forEach(function removeComments(key) {
        delete clone[key].commentary;
      });

      // Stringify and keep it readable.
      return JSON.stringify(clone, null, 2);
    }

    /**
     * Composer function for cfg according HAProxy and adds comments.
     *
     * @param {Object} data
     * @returns {String}
     * @api private
     */
  , cfg: function cfgComposer(data) {
      return Object.keys(data).reduce(function addSections(result, key) {
        var current = data[key];

        return result + Object.keys(current).reduce(function addNames(named, name) {
          var sub = current[name]
            , comm = sub.commentary;

          // Output section and main comments.
          if (comm && comm.pre) result += '# '+ comm.pre +'\n';
          named += key + (name !== 'general' ? ' ' + name + '\n' : '\n');

          // Output section keys and values.
          return named += Object.keys(sub).reduce(function addKeys(section, key) {
            if (key === 'commentary') return section;

            // Check if the key has multiple values stored as array, otherwise
            // just add the key and value and comment (if required).
            if (Object.prototype.toString.call(sub[key]) === '[object Array]') {
              var length = sub[key].length;

              for (var i = 0; i < length; i++) {
                section += '    '+ key + (sub[key][i].length ? ' '+ sub[key][i] : '');
                section += (i === 1 && comm && comm[key] ? ' # ' + comm[key] + '\n' : '\n');
              }

              return section;
            } else {
              return section += '    '+ key + (sub[key].length ? ' '+ sub[key] : '')
                + (comm && comm[key] ? ' # ' + comm[key] + '\n' : '\n');
            }
          }, '') + '\n';
        }, '');
      }, '');
    }
};

/**
 * Parse functions.
 */
var parse = {
    json: JSON.parse

    /**
     * Parse the content from .cfg file.
     *
     * @param {String} data cfg content
     * @return {Object} results
     * @api private
     */
  , cfg: function cfgParser(data) {
      var current, hash, section, key, name;

      data.split(/\n/).forEach(function parse(line) {
        line = line.trim();

        // No content on line just return.
        if (!line.length) return;

        // Keep track of the section.
        section = names.filter(findKey.bind(this, line))[0];
        if (section) {
          name = line.split(' ')[1]; // Named section support.
          return current = section;
        }

        // Check content against known keys in current section.
        key = keys[current].filter(findKey.bind(this, line))[0];
        if (!key.length) return;

        // Store the value if we got a key match, and add the comment (if present).
        hash = line.split('#');
        parser[current](name).add(key, hash[0].substr(key.length).trim())(hash[1]);
      });

      return config;
    }
};

/**
 * Is config key the first part of the line?
 *
 * @param {String} line of content
 * @param {String} key in config
 * @return {Boolean}
 * @api private
 */
function findKey(line, key) {
  return line.indexOf(key) === 0;
}

/**
 * Add comment to section-key combinations. General section comments are added
 * to `commentary.pre`. Return early if there is no actual text.
 *
 * @param {String} section predefined section
 * @param {String} key
 * @param {String} text
 * @return {String} text
 * @api private
 */
function comment(section, name, key, text) {
  var element, sub;

  // Do add comments, will you!
  if (!text) return;

  config[section] = element = config[section] || {};
  element[name] = sub = element[name] || {};
  sub.commentary = sub.commentary || {};

  return sub.commentary[key] = text.trim();
}

/**
 * Change config strings to suitable function names.
 *
 * @param {String} value function name
 * @return {String}
 * @api private
 */
function functionalize(value) {
  return value.toLowerCase().replace(/[^a-z]/g, '');
}

/**
 * Get value of the section-key combination.
 *
 * @param {String} section predefined section
 * @param {String} key
 * @return {String} key value
 * @api private
 */
function get(section, name, key) {
  return config[section][name][key];
}

/**
 * Adds content to key, transforms value to array if required
 *
 * @param {String} section predefined section
 * @param {String} key
 * @param {String} value
 * @return {Object} bind comment to key.
 * @api private
 */
function add(section, name, key, value) {
  var element, sub;

  // Check if the current key is allowed to be set on the section.
  if (!~keys[section].indexOf(key)) return;

  config[section] = element = config[section] || {};
  element[name] = sub = element[name] || {};

  // If this key is undefined just call set.
  if (!sub[key]) return set(section, name, key, value);

  // Convert to array so we can just push to it.
  if (sub[key] && typeof sub[key] === 'string') {
    sub[key] = [sub[key]];
  }

  // Add the value
  sub[key].push(value);

  // Expose comment function bound to key.
  return comment.bind(comment, section, key);
}

/**
 * Set the section-key combination to value.
 *
 * @param {String} section predefined section
 * @param {String} key
 * @param {String} value
 * @return {Object} bind comment to key.
 * @api private
 */
function set(section, name, key, value) {
  var element, sub;

  // Check if the current key is allowed to be set on the section.
  if (!~keys[section].indexOf(key)) return;

  config[section] = element = config[section] || {};
  element[name] = sub = element[name] || {};
  sub[key] = value;

  // Expose comment function bound to key.
  return comment.bind(comment, section, name, key);
}

/**
 * Read the config from file and return parsed config to callback.
 *
 * @param {String} location file location
 * @param {Function} callback
 * @api public
 */
parser.read = function read(location, callback) {
  var type = path.extname(location).substr(1);
  if (!(type in compose)) {
    throw new Error('Supplied file with extension: '+ type +' cannot be parsed');
  }

  // Read the file and pull content through the right parser.
  fs.readFile(location, 'utf-8', function parseFile(err, data) {
    if (err) throw err;

    callback(null, parse[type].call(this, data));
  });
};

/**
 * Verify the current config by using HAProxies check.
 */
parser.verify = function verify() {
  // Spawn child and use haproxy -c -f </tmp/config>
};

/**
 * Write the config to file, composer type is aquired from file extension.
 *
 * @param {String} location file location
 * @param {Function} callback
 * @api public
 */
parser.write = function write(location, callback) {
  var type = path.extname(location).substr(1);
  if (!(type in compose)) type = 'json';

  fs.writeFile(location, compose[type].call(this, parser.config), callback);
};

/**
 * Reset and clear the current config
 *
 * @api public
 */
parser.reset = function reset() {
  parser.config = config = {};
};

/**
 * Generate allowed config keys per section from the bitmasks.
 */
names.forEach(function prepareKeys(section) {
  var mask = sections[section]
    , current;

  Object.keys(maps).forEach(function bitmask(bit) {
    current = keys[section] || [];
    if (mask & +bit) keys[section] = current.concat(maps[bit]);
  });
});

/**
 * Generate some helper methods on each section to quickly set and get values.
 */
names.forEach(function prepareFunctions(section) {
  parser[section] = function setup(name) {
    // Defaults have no name parameter, but for eaz and consistency we
    // are gonna pretend it has a general name section.
    if (!name || section === 'defaults') name = 'general';

    // Add getters and setters to each section.
    var result = {};
    result.__proto__ = {
      get: get.bind(get, section, name),
      set: set.bind(set, section, name),
      add: add.bind(add, section, name),
      comment: comment.bind(comment, section, name, 'pre')
    };

    // Also add camelCased proxies for each key in the section.
    keys[section].forEach(function addProxies(key) {
      result.__proto__[functionalize(key)] = set.bind(set, section, name, key);
    });

    return result;
  };
});

/**
 * Expose additional modules while testing.
 */
if (process.env.NODE_ENV === 'testing') {
  parser.set = set;
  parser.get = get;
  parser.add = add;
  parser.parse = parse;
  parser.compose = compose;
  parser.comment = comment;
  parser.functionalize = functionalize;
  parser.findKey = findKey;
}
<commit_msg>[codestyle] Don't use JSDoc for non function commenting, this makes API generation cleaner [codestyle] Use `exports` instead of `parser` which is probably cleaner [fix] Never throw errors @swaagie [api] Implemented a draft implementation of the `verify` method.<commit_after>'use strict';

//
// Native modules.
//
var path = require('path')
  , fs = require('fs');

//
// Required defaults.
//
var defaults = require('./config')
  , sections = defaults.sections
  , names = Object.keys(sections)
  , maps = defaults.keys
  , keys = {}
  , config;

//
// Our process orchestrator which we need to verification
//
var Orchestrator = require('./orchestrator');

//
// Config storage.
//
exports.config = config = Object.create(null);

//
// Composer functions.
//
var compose = {
    /**
     * Composer function for JSON, strips comments and utilizes stringify.
     *
     * @param {Object} data
     * @returns {String} stringified data.
     * @api private
     */
    json: function stringify(data) {
      var clone = JSON.parse(JSON.stringify(data));

      // Remove commentary as it has no place in JSON.
      Object.keys(clone).forEach(function removeComments(key) {
        delete clone[key].commentary;
      });

      // Stringify and keep it readable.
      return JSON.stringify(clone, null, 2);
    }

    /**
     * Composer function for cfg according HAProxy and adds comments.
     *
     * @param {Object} data
     * @returns {String}
     * @api private
     */
  , cfg: function cfgComposer(data) {
      return Object.keys(data).reduce(function addSections(result, key) {
        var current = data[key];

        return result + Object.keys(current).reduce(function addNames(named, name) {
          var sub = current[name]
            , comm = sub.commentary;

          // Output section and main comments.
          if (comm && comm.pre) result += '# '+ comm.pre +'\n';
          named += key + (name !== 'general' ? ' ' + name + '\n' : '\n');

          // Output section keys and values.
          return named += Object.keys(sub).reduce(function addKeys(section, key) {
            if (key === 'commentary') return section;

            // Check if the key has multiple values stored as array, otherwise
            // just add the key and value and comment (if required).
            if (Object.prototype.toString.call(sub[key]) === '[object Array]') {
              var length = sub[key].length;

              for (var i = 0; i < length; i++) {
                section += '    '+ key + (sub[key][i].length ? ' '+ sub[key][i] : '');
                section += (i === 1 && comm && comm[key] ? ' # ' + comm[key] + '\n' : '\n');
              }

              return section;
            } else {
              return section += '    '+ key + (sub[key].length ? ' '+ sub[key] : '')
                + (comm && comm[key] ? ' # ' + comm[key] + '\n' : '\n');
            }
          }, '') + '\n';
        }, '');
      }, '');
    }
};

//
// Parse functions.
//
var parse = {
    json: JSON.parse

    /**
     * Parse the content from .cfg file.
     *
     * @param {String} data cfg content
     * @return {Object} results
     * @api private
     */
  , cfg: function cfgParser(data) {
      var current, hash, section, key, name;

      data.split(/\n/).forEach(function parse(line) {
        line = line.trim();

        // No content on line just return.
        if (!line.length) return;

        // Keep track of the section.
        section = names.filter(findKey.bind(this, line))[0];
        if (section) {
          name = line.split(' ')[1]; // Named section support.
          return current = section;
        }

        // Check content against known keys in current section.
        key = keys[current].filter(findKey.bind(this, line))[0];
        if (!key.length) return;

        // Store the value if we got a key match, and add the comment (if present).
        hash = line.split('#');
        exports[current](name).add(key, hash[0].substr(key.length).trim())(hash[1]);
      });

      return config;
    }
};

/**
 * Is config key the first part of the line?
 *
 * @param {String} line of content
 * @param {String} key in config
 * @return {Boolean}
 * @api private
 */
function findKey(line, key) {
  return line.indexOf(key) === 0;
}

/**
 * Add comment to section-key combinations. General section comments are added
 * to `commentary.pre`. Return early if there is no actual text.
 *
 * @param {String} section predefined section
 * @param {String} key
 * @param {String} text
 * @return {String} text
 * @api private
 */
function comment(section, name, key, text) {
  var element, sub;

  // Do add comments, will you!
  if (!text) return;

  config[section] = element = config[section] || {};
  element[name] = sub = element[name] || {};
  sub.commentary = sub.commentary || {};

  return sub.commentary[key] = text.trim();
}

/**
 * Change config strings to suitable function names.
 *
 * @param {String} value function name
 * @return {String}
 * @api private
 */
function functionalize(value) {
  return value.toLowerCase().replace(/[^a-z]/g, '');
}

/**
 * Get value of the section-key combination.
 *
 * @param {String} section predefined section
 * @param {String} key
 * @return {String} key value
 * @api private
 */
function get(section, name, key) {
  return config[section][name][key];
}

/**
 * Adds content to key, transforms value to array if required
 *
 * @param {String} section predefined section
 * @param {String} key
 * @param {String} value
 * @return {Object} bind comment to key.
 * @api private
 */
function add(section, name, key, value) {
  var element, sub;

  // Check if the current key is allowed to be set on the section.
  if (!~keys[section].indexOf(key)) return;

  config[section] = element = config[section] || {};
  element[name] = sub = element[name] || {};

  // If this key is undefined just call set.
  if (!sub[key]) return set(section, name, key, value);

  // Convert to array so we can just push to it.
  if (sub[key] && typeof sub[key] === 'string') {
    sub[key] = [sub[key]];
  }

  // Add the value
  sub[key].push(value);

  // Expose comment function bound to key.
  return comment.bind(comment, section, key);
}

/**
 * Set the section-key combination to value.
 *
 * @param {String} section predefined section
 * @param {String} key
 * @param {String} value
 * @return {Object} bind comment to key.
 * @api private
 */
function set(section, name, key, value) {
  var element, sub;

  // Check if the current key is allowed to be set on the section.
  if (!~keys[section].indexOf(key)) return;

  config[section] = element = config[section] || {};
  element[name] = sub = element[name] || {};
  sub[key] = value;

  // Expose comment function bound to key.
  return comment.bind(comment, section, name, key);
}

/**
 * Read the config from file and return parsed config to callback.
 *
 * @param {String} location file location
 * @param {Function} callback
 * @api public
 */
exports.read = function read(location, callback) {
  var type = path.extname(location).substr(1);

  if (!(type in compose)) {
    callback(new Error('Supplied file with extension: '+ type +' cannot be parsed'));
    return exports;
  }

  // Read the file and pull content through the right parser.
  fs.readFile(location, 'utf-8', function parseFile(err, data) {
    if (err) return callback(err);

    callback(undefined, parse[type].call(this, data));
  });

  return exports;
};

/**
 * Verify the current config by using HAProxies check.
 *
 * @param {Function} callback
 * @api public
 */
exports.verify = function verify(callback) {
  var tmp = '/tmp/haproxy.'+ Math.random().toString(36).substring(2).toUpperCase()
    , orchestrator = new Orchestrator({ config: tmp })
    , data = compose.cfg(config);

  fs.writeFile(tmp, data, function hollaback(err) {
    if (err) return callback(err);

    orchestrator.verify(function verification() {
      callback.apply(exports, arguments);

      //
      // We don't care if it fails or not, each file would have a unique name.
      // And we don't really need to remove it from the file system but it's
      // just `nice` do some additional cleanup
      //
      fs.unlink(tmp, function likeigiveafuck() {});
    });
  });

  return exports;
};

/**
 * Write the config to file, composer type is aquired from file extension.
 *
 * @param {String} location file location
 * @param {Function} callback
 * @api public
 */
exports.write = function write(location, callback) {
  var type = path.extname(location).substr(1);
  if (!(type in compose)) type = 'json';

  fs.writeFile(location, compose[type].call(this, exports.config), callback);
  return exports;
};

/**
 * Reset and clear the current config
 *
 * @api public
 */
exports.reset = function reset() {
  exports.config = config = Object.create(null);
  return exports;
};

//
// Generate allowed config keys per section from the bitmasks.
//
names.forEach(function prepareKeys(section) {
  var mask = sections[section]
    , current;

  Object.keys(maps).forEach(function bitmask(bit) {
    current = keys[section] || [];
    if (mask & +bit) keys[section] = current.concat(maps[bit]);
  });
});

//
// Generate some helper methods on each section to quickly set and get values.
//
names.forEach(function prepareFunctions(section) {
  exports[section] = function setup(name) {
    // Defaults have no name parameter, but for eaz and consistency we
    // are gonna pretend it has a general name section.
    if (!name || section === 'defaults') name = 'general';

    // Add getters and setters to each section.
    var result = {};
    result.__proto__ = {
      get: get.bind(get, section, name),
      set: set.bind(set, section, name),
      add: add.bind(add, section, name),
      comment: comment.bind(comment, section, name, 'pre')
    };

    // Also add camelCased proxies for each key in the section.
    keys[section].forEach(function addProxies(key) {
      result.__proto__[functionalize(key)] = set.bind(set, section, name, key);
    });

    return result;
  };
});

//
// Expose additional modules while testing.
//
if (process.env.NODE_ENV === 'testing') {
  exports.set = set;
  exports.get = get;
  exports.add = add;
  exports.parse = parse;
  exports.compose = compose;
  exports.comment = comment;
  exports.functionalize = functionalize;
  exports.findKey = findKey;
}
<|endoftext|>


<commit_before>var nssocket = require('nssocket');
var database = require('./database-service');
var argv = require('minimist')(process.argv.slice(2));

Object.keys(argv).forEach(function(key) {
  switch (key) {
    case 'port':
      process.env.PORT = argv.port;
      break;
    case 'dir' :
      process.env.DB_PATH = argv.dir;
      break;
  }
});

if (!process.env.DB_PATH) {
  console.error('missing required flag --dir');
  process.exit();
}

/**
 * Socket handler.
 *
 * @param socket
 */
function socketHandler(socket) {
  // fields are request - data, methodType, reqId, fileName,
  // send event: call_cb_$reqId.
  socket.data('dbcall', function(request) {
    database.processRequest(request, function(response) {
      response.reqId = request.reqId;
      socket.send('dbcall_cb_' + request.reqId, response);
    });
  });
}

/**
 * Main method for starting a server.
 */
function main() {
  var port = process.env.PORT || 7000;
  nssocket.createServer(socketHandler).listen(port);

  console.log('Server listening at port ' + port);
}

// call main method.
main();<commit_msg>matched client callback id<commit_after>var nssocket = require('nssocket');
var database = require('./database-service');
var argv = require('minimist')(process.argv.slice(2));

if (!argv.dir) {
  console.error('missing required flag --dir');
  process.exit();
}

process.env.DB_PATH = argv.dir;

/**
 * Socket handler.
 *
 * @param socket
 */
function socketHandler(socket) {
  // fields are request - data, methodType, reqId, fileName,
  // send event: call_cb.
  socket.data('dbcall', function(request) {
    database.processRequest(request, function(response) {
      socket.send('dbcall_cb', response);
    });
  });
}

/**
 * Main method for starting a server.
 */
function main() {
  var port = argv.port || 7000;
  nssocket.createServer(socketHandler).listen(port);

  console.log('Server listening at port ' + port);
}

// call main method.
main();<|endoftext|>


<commit_before>'use strict';

const http = require('http');
const zlib = require('zlib');
const Stream = require('stream');

const serveStatic = require('serve-static');
const ClusterWorker = require('flora-cluster').Worker;
const errors = require('flora-errors');

const Api = require('./api');
const Response = require('./response');
const httpToFloraRequest = require('./url-parser');

const NotFoundError = errors.NotFoundError;

/**
 * Server (or cluster worker) process.
 *
 * Provides a {@link https://nodejs.org/api/http.html#http_class_http_server|HTTP server} that listens to a specific
 * port and handles requests.
 */
class Server {
    /**
     * @param {string} configPath   - Location of resource configs
     */
    constructor(configPath) {
        if (!configPath) throw new Error('Master must be called with a configPath parameter');

        this.api = null;
        this.log = null;

        this._httpServer = null;
        this._staticHandler = null;
        this._configPath = require.resolve(configPath);
        this._clusterWorker = null;
        this._plugins = [];
    }

    /**
     * Run the worker process.
     *
     * Initializes a new instance of {@link Api} by resolving the configPath supplied
     * in the constructor. Attaches to a flora-cluster worker (if we are in a cluster)
     * and creates a new {@link https://nodejs.org/api/http.html#http_class_http_server|HTTP server}.
     *
     * @param {Function} callback
     */
    run(callback) {
        // eslint-disable-next-line global-require,import/no-dynamic-require
        const config = require(this._configPath);

        callback = callback || function nop() {};

        this._clusterWorker = new ClusterWorker({
            shutdownTimeout: config.shutdownTimeout,
            log: config.log
        });

        this.api = new Api();
        this.api.clusterWorker = this._clusterWorker;
        this.api.init(config, (err) => {
            this.log = this.api.log;

            if (err) {
                this.log.fatal(err, 'Error initializing Flora');
                return callback(err);
            }

            this._plugins.forEach((plugin) => {
                const [obj, options] = plugin;
                this.log.debug(`Registering plugin "${obj.name || '(unnamed)'}"`);
                obj.register(this.api, options);
            });

            if (config.staticPath) {
                this._staticHandler = serveStatic(config.staticPath);
            }

            if (!config.port) {
                this.log.warn('No port in configuration, not starting HTTP server');
                return callback(null);
            }

            this._httpServer = http.createServer(this._handleRequest.bind(this));

            this._clusterWorker.on('close', () => this.close());

            this._clusterWorker.attach(this._httpServer);

            return this._httpServer.listen(config.port, () => {
                this._clusterWorker.ready();
                this.log.info('Flora server running on port %d', config.port);
                callback(null);
            });
        });
    }

    /**
     * Gracefully shutdown the server.
     *
     * @param {Function=} callback
     */
    close(callback) {
        callback = callback || (() => {});

        if (!this.api) return callback(new Error('Not running'));

        if (this._httpServer) {
            this.log.info('Closing HTTP server');
            return this._httpServer.close((err) => {
                // Callback will get the error from httpServer#close,
                // regardless what Api#close returns.);
                this.api.close(() => callback(err));
            });
        }

        return this.api.close(callback);
    }

    /**
     * Register a plugin.
     *
     * The plugin has to be an object with a "register" function property, which is called with
     * parameters (api, options).
     *
     * @param {Object} plugin
     * @param {Object} options
     */
    register(plugin, options) {
        this._plugins.push([plugin, options]);
        if (this.api) this.api.register(plugin, options);
    }

    /**
     * Handle a HTTP request.
     *
     * Parse the request path and parameters, search for the required resource
     * and execute the resource, build and send the response.
     *
     * @param {http.Request} httpRequest
     * @param {http.Response} httpResponse
     * @private
     */
    _handleRequest(httpRequest, httpResponse) {
        this.api.emit('httpRequest', { httpRequest, httpResponse }, (extErr) => {
            if (extErr) return this._sendError(extErr, httpRequest, httpResponse);

            if (httpRequest.method === 'OPTIONS') {
                // Abort execution of OPTIONS requests here, send 200 for now
                httpResponse.writeHead(200);
                return httpResponse.end();
            }

            return httpToFloraRequest(httpRequest)
                .then((floraRequest) => {
                    if (!floraRequest) {
                        if (this._staticHandler) {
                            return this._staticHandler(httpRequest, httpResponse, () => {
                                this._sendError(
                                    new NotFoundError(`URL "${httpRequest.url}" not found (not a valid resource url)`),
                                    httpRequest, httpResponse);
                            });
                        }
                        return this._sendError(
                            new NotFoundError(`URL "${httpRequest.url}" not found (not a valid resource url)`),
                            httpRequest, httpResponse);
                    }

                    if (!this.api.getResource(floraRequest.resource) && this._staticHandler) {
                        return this._staticHandler(httpRequest, httpResponse, () => {
                            this._sendError(
                                new NotFoundError(`Resource "${floraRequest.resource}" not found`),
                                httpRequest, httpResponse);
                        });
                    }

                    return this.api.execute(floraRequest, (err, response) => {
                        if (err) {
                            err.response = response;
                            return this._sendError(err, httpRequest, httpResponse);
                        }
                        return this._sendResponse(response, httpRequest, httpResponse);
                    });
                })
                .catch(err => this._sendError(err, httpRequest, httpResponse));
        });
    }

    /**
     * @param {FloraError} err
     * @param {http.Request} httpRequest
     * @param {http.Response} httpResponse
     * @private
     */
    _sendError(err, httpRequest, httpResponse) {
        const response = err.response || new Response();
        response.meta.statusCode = err.httpStatusCode || new errors.FloraError().httpStatusCode;
        response.error = errors.format(err, { exposeErrors: this.api.config.exposeErrors });

        this._sendResponse(response, httpRequest, httpResponse);
    }

    /**
     * @param {Object} response
     * @param {http.Request} httpRequest
     * @param {http.Response} httpResponse
     * @private
     */
    _sendResponse(response, httpRequest, httpResponse) {
        const headers = response.meta.headers;
        let stream = httpResponse;
        const acceptEncoding = httpRequest.headers['accept-encoding'] || '';

        if (!headers['Content-Type']) headers['Content-Type'] = 'application/json; charset=utf-8';

        if (acceptEncoding.match(/\bgzip\b/)) {
            headers['Content-Encoding'] = 'gzip';
            stream = zlib.createGzip();
            stream.pipe(httpResponse);
        } else if (acceptEncoding.match(/\bdeflate\b/)) {
            headers['Content-Encoding'] = 'deflate';
            stream = zlib.createDeflate();
            stream.pipe(httpResponse);
        }

        try {
            let json = null;

            if (!(response.data instanceof Stream.Readable)) {
                json = JSON.stringify({
                    meta: response.meta,
                    cursor: response.cursor,
                    error: response.error,
                    data: response.data
                });
            }

            // measure duration as late as possible - after JSON.stringify, right before writeHead:
            if (httpRequest.flora && httpRequest.flora.startTime) {
                const hrtime = process.hrtime(httpRequest.flora.startTime);
                headers['X-Response-Time'] = Math.round(((hrtime[0] * 1000) + (hrtime[1] / 1000000)) * 1000) / 1000;
            }

            httpResponse.writeHead(response.meta.statusCode, headers);

            if (json !== null) {
                stream.end(json);
            } else {
                response.data.pipe(stream);
            }
        } catch (e) {
            this.log.warn(e, 'Error while sending response');
        }
    }
}

module.exports = Server;
<commit_msg>Shut down clusterWorker on initialization errors (fixes hanging workers)<commit_after>'use strict';

const http = require('http');
const zlib = require('zlib');
const Stream = require('stream');

const serveStatic = require('serve-static');
const ClusterWorker = require('flora-cluster').Worker;
const errors = require('flora-errors');

const Api = require('./api');
const Response = require('./response');
const httpToFloraRequest = require('./url-parser');

const NotFoundError = errors.NotFoundError;

/**
 * Server (or cluster worker) process.
 *
 * Provides a {@link https://nodejs.org/api/http.html#http_class_http_server|HTTP server} that listens to a specific
 * port and handles requests.
 */
class Server {
    /**
     * @param {string} configPath   - Location of resource configs
     */
    constructor(configPath) {
        if (!configPath) throw new Error('Master must be called with a configPath parameter');

        this.api = null;
        this.log = null;

        this._httpServer = null;
        this._staticHandler = null;
        this._configPath = require.resolve(configPath);
        this._clusterWorker = null;
        this._plugins = [];
    }

    /**
     * Run the worker process.
     *
     * Initializes a new instance of {@link Api} by resolving the configPath supplied
     * in the constructor. Attaches to a flora-cluster worker (if we are in a cluster)
     * and creates a new {@link https://nodejs.org/api/http.html#http_class_http_server|HTTP server}.
     *
     * @param {Function} callback
     */
    run(callback) {
        // eslint-disable-next-line global-require,import/no-dynamic-require
        const config = require(this._configPath);

        callback = callback || function nop() {};

        this._clusterWorker = new ClusterWorker({
            shutdownTimeout: config.shutdownTimeout,
            log: config.log
        });

        this.api = new Api();
        this.api.clusterWorker = this._clusterWorker;
        this.api.init(config, (err) => {
            this.log = this.api.log;

            if (err) {
                this.log.fatal(err, 'Error initializing Flora');
                this._clusterWorker.shutdown();
                return callback(err);
            }

            this._plugins.forEach((plugin) => {
                const [obj, options] = plugin;
                this.log.debug(`Registering plugin "${obj.name || '(unnamed)'}"`);
                obj.register(this.api, options);
            });

            if (config.staticPath) {
                this._staticHandler = serveStatic(config.staticPath);
            }

            if (!config.port) {
                this.log.warn('No port in configuration, not starting HTTP server');
                return callback(null);
            }

            this._httpServer = http.createServer(this._handleRequest.bind(this));

            this._clusterWorker.on('close', () => this.close());

            this._clusterWorker.attach(this._httpServer);

            return this._httpServer.listen(config.port, () => {
                this._clusterWorker.ready();
                this.log.info('Flora server running on port %d', config.port);
                callback(null);
            });
        });
    }

    /**
     * Gracefully shutdown the server.
     *
     * @param {Function=} callback
     */
    close(callback) {
        callback = callback || (() => {});

        if (!this.api) return callback(new Error('Not running'));

        if (this._httpServer) {
            this.log.info('Closing HTTP server');
            return this._httpServer.close((err) => {
                // Callback will get the error from httpServer#close,
                // regardless what Api#close returns.);
                this.api.close(() => callback(err));
            });
        }

        return this.api.close(callback);
    }

    /**
     * Register a plugin.
     *
     * The plugin has to be an object with a "register" function property, which is called with
     * parameters (api, options).
     *
     * @param {Object} plugin
     * @param {Object} options
     */
    register(plugin, options) {
        this._plugins.push([plugin, options]);
        if (this.api) this.api.register(plugin, options);
    }

    /**
     * Handle a HTTP request.
     *
     * Parse the request path and parameters, search for the required resource
     * and execute the resource, build and send the response.
     *
     * @param {http.Request} httpRequest
     * @param {http.Response} httpResponse
     * @private
     */
    _handleRequest(httpRequest, httpResponse) {
        this.api.emit('httpRequest', { httpRequest, httpResponse }, (extErr) => {
            if (extErr) return this._sendError(extErr, httpRequest, httpResponse);

            if (httpRequest.method === 'OPTIONS') {
                // Abort execution of OPTIONS requests here, send 200 for now
                httpResponse.writeHead(200);
                return httpResponse.end();
            }

            return httpToFloraRequest(httpRequest)
                .then((floraRequest) => {
                    if (!floraRequest) {
                        if (this._staticHandler) {
                            return this._staticHandler(httpRequest, httpResponse, () => {
                                this._sendError(
                                    new NotFoundError(`URL "${httpRequest.url}" not found (not a valid resource url)`),
                                    httpRequest, httpResponse);
                            });
                        }
                        return this._sendError(
                            new NotFoundError(`URL "${httpRequest.url}" not found (not a valid resource url)`),
                            httpRequest, httpResponse);
                    }

                    if (!this.api.getResource(floraRequest.resource) && this._staticHandler) {
                        return this._staticHandler(httpRequest, httpResponse, () => {
                            this._sendError(
                                new NotFoundError(`Resource "${floraRequest.resource}" not found`),
                                httpRequest, httpResponse);
                        });
                    }

                    return this.api.execute(floraRequest, (err, response) => {
                        if (err) {
                            err.response = response;
                            return this._sendError(err, httpRequest, httpResponse);
                        }
                        return this._sendResponse(response, httpRequest, httpResponse);
                    });
                })
                .catch(err => this._sendError(err, httpRequest, httpResponse));
        });
    }

    /**
     * @param {FloraError} err
     * @param {http.Request} httpRequest
     * @param {http.Response} httpResponse
     * @private
     */
    _sendError(err, httpRequest, httpResponse) {
        const response = err.response || new Response();
        response.meta.statusCode = err.httpStatusCode || new errors.FloraError().httpStatusCode;
        response.error = errors.format(err, { exposeErrors: this.api.config.exposeErrors });

        this._sendResponse(response, httpRequest, httpResponse);
    }

    /**
     * @param {Object} response
     * @param {http.Request} httpRequest
     * @param {http.Response} httpResponse
     * @private
     */
    _sendResponse(response, httpRequest, httpResponse) {
        const headers = response.meta.headers;
        let stream = httpResponse;
        const acceptEncoding = httpRequest.headers['accept-encoding'] || '';

        if (!headers['Content-Type']) headers['Content-Type'] = 'application/json; charset=utf-8';

        if (acceptEncoding.match(/\bgzip\b/)) {
            headers['Content-Encoding'] = 'gzip';
            stream = zlib.createGzip();
            stream.pipe(httpResponse);
        } else if (acceptEncoding.match(/\bdeflate\b/)) {
            headers['Content-Encoding'] = 'deflate';
            stream = zlib.createDeflate();
            stream.pipe(httpResponse);
        }

        try {
            let json = null;

            if (!(response.data instanceof Stream.Readable)) {
                json = JSON.stringify({
                    meta: response.meta,
                    cursor: response.cursor,
                    error: response.error,
                    data: response.data
                });
            }

            // measure duration as late as possible - after JSON.stringify, right before writeHead:
            if (httpRequest.flora && httpRequest.flora.startTime) {
                const hrtime = process.hrtime(httpRequest.flora.startTime);
                headers['X-Response-Time'] = Math.round(((hrtime[0] * 1000) + (hrtime[1] / 1000000)) * 1000) / 1000;
            }

            httpResponse.writeHead(response.meta.statusCode, headers);

            if (json !== null) {
                stream.end(json);
            } else {
                response.data.pipe(stream);
            }
        } catch (e) {
            this.log.warn(e, 'Error while sending response');
        }
    }
}

module.exports = Server;
<|endoftext|>