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

var util = require('./util')
  , transports = require('./transports')
  , EventEmitter = require('./event-emitter')

/**
 * Module exports.
 */

module.exports = Socket;

/**
 * Socket constructor.
 *
 * @param {Object} options
 * @api public
 */

function Socket (opts) {
  if ('string' == typeof opts) {
    var uri = util.parseUri(opts);
    opts = arguments[1] || {};
    opts.host = uri.host;
    opts.secure = uri.scheme == 'https' || uri.scheme == 'wss';
    opts.port = uri.port || (opts.secure ? 443 : 80);
  }

  opts = opts || {};
  this.secure = opts.secure || false;
  this.host = opts.host || opts.hostname || 'localhost';
  this.port = opts.port || 80;
  this.query = opts.query || {};
  this.query.uid = rnd();
  this.upgrade = false !== opts.upgrade;
  this.basePath = opts.basePath || '/engine.io';
  this.path = opts.path || '';
  this.forceJSONP = !!opts.forceJSONP;
  this.flashPath = opts.flashPath || '';
  this.transports = opts.transports || ['polling', 'websocket', 'flashsocket'];
  this.readyState = '';
  this.writeBuffer = [];
  this.open();
};

/**
 * Inherits from EventEmitter.
 */

util.inherits(Socket, EventEmitter);

/**
 * Creates transport of the given type.
 *
 * @param {String} transport name
 * @return {Transport}
 * @api private
 */

Socket.prototype.createTransport = function (name) {
  // debug: creating transport "%s", name
  var query = clone(this.query)
  query.transport = name;

  if (this.id) {
    query.sid = this.id;
  }

  var transport = new transports[name]({
      host: this.host
    , port: this.port
    , secure: this.secure
    , path: this.basePath + this.path
    , query: query
    , forceJSONP: this.forceJSONP
    , flashPath: this.flashPath
  });

  return transport;
};

function clone (obj) {
  var o = {};
  for (var i in obj) {
    if (obj.hasOwnProperty(i)) {
      o[i] = obj[i];
    }
  }
  return o;
}

/**
 * Initializes transport to use and starts probe.
 *
 * @api private
 */

Socket.prototype.open = function () {
  this.readyState = 'opening';
  var transport = this.createTransport(this.transports[0]);
  transport.open();
  this.setTransport(transport);
};

/**
 * Sets the current transport. Disables the existing one (if any).
 *
 * @api private
 */

Socket.prototype.setTransport = function (transport) {
  var self = this;

  if (this.transport) {
    // debug: clearing existing transport
    this.transport.removeAllListeners();
  }

  // set up transport
  this.transport = transport;

  // set up transport listeners
  transport
    .on('drain', function () {
      self.flush();
    })
    .on('packet', function (packet) {
      self.onPacket(packet);
    })
    .on('error', function (e) {
      self.onError(e);
    })
    .on('close', function () {
      self.onClose('transport close');
    })
};

/**
 * Probes a transport.
 *
 * @param {String} transport name
 * @api private
 */

Socket.prototype.probe = function (name) {
  // debug: probing transport "%s", name
  var transport = this.createTransport(name, { probe: 1 })
    , self = this

  transport.once('open', function () {
    // debug: probe transport "%s" opened - pinging, name
    transport.send([{ type: 'ping', data: 'probe' }]);
    transport.once('packet', function (msg) {
      if ('pong' == msg.type && 'probe' == msg.data) {
        // debug: probe transport "%s" pong - upgrading, name
        self.upgrading = true;
        self.emit('upgrading', transport);

        // debug: pausing current transport "%s", self.transport.name
        self.transport.pause(function () {
          if ('closed' == self.readyState || 'closing' == self.readyState) return;
          // debug: changing transport and sending upgrade packet
          self.emit('upgrade', transport);
          self.setTransport(transport);
          transport.send([{ type: 'upgrade' }]);
          transport = null;
          self.upgrading = false;
          self.flush();
        });
      } else {
        // debug: probe transport "%s" failed, name
        var err = new Error('probe error');
        err.transport = transport.name;
        self.emit('error', err);
      }
    });
  });

  transport.open();

  this.once('close', function () {
    if (transport) {
      // debug: socket closed prematurely - aborting probe
      transport.close();
      transport = null;
    }
  });

  this.once('upgrading', function (to) {
    if (transport && to.name != transport.name) {
      // debug: "%s" works - aborting "%s", to.name, transport.name
      transport.close();
      transport = null;
    }
  });
};

/**
 * Called when connection is deemed open.
 *
 * @api public
 */

Socket.prototype.onOpen = function () {
  // debug: socket open
  this.readyState = 'open';
  this.emit('open');
  this.onopen && this.onopen.call(this);
  this.flush();

  if (this.upgrade && this.transport.pause) {
    // debug: starting upgrade probes
    for (var i = 0, l = this.upgrades.length; i < l; i++) {
      this.probe(this.upgrades[i]);
    }
  }
};

/**
 * Handles a packet.
 *
 * @api private
 */

Socket.prototype.onPacket = function (packet) {
  if ('opening' == this.readyState || 'open' == this.readyState) {
    // debug: socket receive: type "%s" | data "%s", packet.type, packet.data
    switch (packet.type) {
      case 'open':
        this.onHandshake(util.parseJSON(packet.data));
        break;

      case 'ping':
        this.sendPacket('pong');
        this.setPingTimeout();
        break;

      case 'error':
        var err = new Error('server error');
        err.code = packet.data;
        this.emit('error', err);
        break;

      case 'message':
        this.emit('message', packet.data);
        this.onmessage && this.onmessage.call(this, { data: packet.data });
        break;
    }
  } else {
    // debug: packet received with socket readyState "%s", this.readyState
  }
};

/**
 * Called upon handshake completion.
 *
 * @param {Object} handshake obj
 * @api private
 */

Socket.prototype.onHandshake = function (data) {
  this.emit('handshake', data);
  this.id = data.sid;
  this.transport.query.sid = data.sid;
  this.upgrades = data.upgrades;
  this.pingTimeout = data.pingTimeout;
  this.onOpen();
  this.setPingTimeout();
};

/**
 * Clears and sets a ping timeout based on the expected ping interval.
 *
 * @api private
 */

Socket.prototype.setPingTimeout = function () {
  clearTimeout(this.pingTimeoutTimer);
  var self = this;
  this.pingTimeoutTimer = setTimeout(function () {
    self.onClose('ping timeout');
  }, this.pingTimeout);
};

/**
 * Flush write buffers.
 *
 * @api private
 */

Socket.prototype.flush = function () {
  if ('closed' != this.readyState && this.transport.writable
    && !this.upgrading && this.writeBuffer.length) {
    // debug: flushing %d packets in socket, this.writeBuffer.length
    this.transport.send(this.writeBuffer);
    this.writeBuffer = [];
  }
};

/**
 * Sends a message.
 *
 * @param {String} message.
 * @return {Socket} for chaining.
 * @api public
 */

Socket.prototype.send = function (msg) {
  this.sendPacket('message', msg);
  return this;
};

/**
 * Sends a packet.
 *
 * @param {String} packet type.
 * @param {String} data.
 * @api private
 */

Socket.prototype.sendPacket = function (type, data) {
  var packet = { type: type, data: data };
  this.writeBuffer.push(packet);
  this.flush();
};

/**
 * Closes the connection.
 *
 * @api private
 */

Socket.prototype.close = function () {
  if ('opening' == this.readyState || 'open' == this.readyState) {
    this.onClose('forced close');
    // debug: socket closing - telling transport to close
    this.transport.close();
  }

  return this;
};

/**
 * Called upon transport error
 *
 * @api private
 */

Socket.prototype.onError = function (err) {
  this.emit('error', err);
  this.onClose('transport error', err);
};

/**
 * Called upon transport close.
 *
 * @api private
 */

Socket.prototype.onClose = function (reason, desc) {
  if ('closed' != this.readyState) {
    // debug: socket close: "%s", reason
    this.readyState = 'closed';
    this.emit('close', reason, desc);
    this.onclose && this.onclose.call(this);
  }
};

/**
 * Generates a random uid.
 *
 * @api private
 */

function rnd () {
  return String(Math.random()).substr(5) + String(Math.random()).substr(5);
}
<commit_msg>replaced control character with space<commit_after>/**
 * Module dependencies.
 */

var util = require('./util')
  , transports = require('./transports')
  , EventEmitter = require('./event-emitter')

/**
 * Module exports.
 */

module.exports = Socket;

/**
 * Socket constructor.
 *
 * @param {Object} options
 * @api public
 */

function Socket (opts) {
  if ('string' == typeof opts) {
    var uri = util.parseUri(opts);
    opts = arguments[1] || {};
    opts.host = uri.host;
    opts.secure = uri.scheme == 'https' || uri.scheme == 'wss';
    opts.port = uri.port || (opts.secure ? 443 : 80);
  }

  opts = opts || {};
  this.secure = opts.secure || false;
  this.host = opts.host || opts.hostname || 'localhost';
  this.port = opts.port || 80;
  this.query = opts.query || {};
  this.query.uid = rnd();
  this.upgrade = false !== opts.upgrade;
  this.basePath = opts.basePath || '/engine.io';
  this.path = opts.path || '';
  this.forceJSONP = !!opts.forceJSONP;
  this.flashPath = opts.flashPath || '';
  this.transports = opts.transports || ['polling', 'websocket', 'flashsocket'];
  this.readyState = '';
  this.writeBuffer = [];
  this.open();
};

/**
 * Inherits from EventEmitter.
 */

util.inherits(Socket, EventEmitter);

/**
 * Creates transport of the given type.
 *
 * @param {String} transport name
 * @return {Transport}
 * @api private
 */

Socket.prototype.createTransport = function (name) {
  // debug: creating transport "%s", name
  var query = clone(this.query)
  query.transport = name;

  if (this.id) {
    query.sid = this.id;
  }

  var transport = new transports[name]({
      host: this.host
    , port: this.port
    , secure: this.secure
    , path: this.basePath + this.path
    , query: query
    , forceJSONP: this.forceJSONP
    , flashPath: this.flashPath
  });

  return transport;
};

function clone (obj) {
  var o = {};
  for (var i in obj) {
    if (obj.hasOwnProperty(i)) {
      o[i] = obj[i];
    }
  }
  return o;
}

/**
 * Initializes transport to use and starts probe.
 *
 * @api private
 */

Socket.prototype.open = function () {
  this.readyState = 'opening';
  var transport = this.createTransport(this.transports[0]);
  transport.open();
  this.setTransport(transport);
};

/**
 * Sets the current transport. Disables the existing one (if any).
 *
 * @api private
 */

Socket.prototype.setTransport = function (transport) {
  var self = this;

  if (this.transport) {
    // debug: clearing existing transport
    this.transport.removeAllListeners();
  }

  // set up transport
  this.transport = transport;

  // set up transport listeners
  transport
    .on('drain', function () {
      self.flush();
    })
    .on('packet', function (packet) {
      self.onPacket(packet);
    })
    .on('error', function (e) {
      self.onError(e);
    })
    .on('close', function () {
      self.onClose('transport close');
    })
};

/**
 * Probes a transport.
 *
 * @param {String} transport name
 * @api private
 */

Socket.prototype.probe = function (name) {
  // debug: probing transport "%s", name
  var transport = this.createTransport(name, { probe: 1 })
    , self = this

  transport.once('open', function () {
    // debug: probe transport "%s" opened - pinging, name
    transport.send([{ type: 'ping', data: 'probe' }]);
    transport.once('packet', function (msg) {
      if ('pong' == msg.type && 'probe' == msg.data) {
        // debug: probe transport "%s" pong - upgrading, name
        self.upgrading = true;
        self.emit('upgrading', transport);

        // debug: pausing current transport "%s", self.transport.name
        self.transport.pause(function () {
          if ('closed' == self.readyState || 'closing' == self.readyState) return;
          // debug: changing transport and sending upgrade packet
          self.emit('upgrade', transport);
          self.setTransport(transport);
          transport.send([{ type: 'upgrade' }]);
          transport = null;
          self.upgrading = false;
          self.flush();
        });
      } else {
        // debug: probe transport "%s" failed, name
        var err = new Error('probe error');
        err.transport = transport.name;
        self.emit('error', err);
      }
    });
  });

  transport.open();

  this.once('close', function () {
    if (transport) {
      // debug: socket closed prematurely - aborting probe
      transport.close();
      transport = null;
    }
  });

  this.once('upgrading', function (to) {
    if (transport && to.name != transport.name) {
      // debug: "%s" works - aborting "%s", to.name, transport.name
      transport.close();
      transport = null;
    }
  });
};

/**
 * Called when connection is deemed open.
 *
 * @api public
 */

Socket.prototype.onOpen = function () {
  // debug: socket open
  this.readyState = 'open';
  this.emit('open');
  this.onopen && this.onopen.call(this);
  this.flush();

  if (this.upgrade && this.transport.pause) {
    // debug: starting upgrade probes
    for (var i = 0, l = this.upgrades.length; i < l; i++) {
      this.probe(this.upgrades[i]);
    }
  }
};

/**
 * Handles a packet.
 *
 * @api private
 */

Socket.prototype.onPacket = function (packet) {
  if ('opening' == this.readyState || 'open' == this.readyState) {
    // debug: socket receive: type "%s" | data "%s", packet.type, packet.data
    switch (packet.type) {
      case 'open':
        this.onHandshake(util.parseJSON(packet.data));
        break;

      case 'ping':
        this.sendPacket('pong');
        this.setPingTimeout();
        break;

      case 'error':
        var err = new Error('server error');
        err.code = packet.data;
        this.emit('error', err);
        break;

      case 'message':
        this.emit('message', packet.data);
        this.onmessage && this.onmessage.call(this, { data: packet.data });
        break;
    }
  } else {
    // debug: packet received with socket readyState "%s", this.readyState
  }
};

/**
 * Called upon handshake completion.
 *
 * @param {Object} handshake obj
 * @api private
 */

Socket.prototype.onHandshake = function (data) {
  this.emit('handshake', data);
  this.id = data.sid;
  this.transport.query.sid = data.sid;
  this.upgrades = data.upgrades;
  this.pingTimeout = data.pingTimeout;
  this.onOpen();
  this.setPingTimeout();
};

/**
 * Clears and sets a ping timeout based on the expected ping interval.
 *
 * @api private
 */

Socket.prototype.setPingTimeout = function () {
  clearTimeout(this.pingTimeoutTimer);
  var self = this;
  this.pingTimeoutTimer = setTimeout(function () {
    self.onClose('ping timeout');
  }, this.pingTimeout);
};

/**
 * Flush write buffers.
 *
 * @api private
 */

Socket.prototype.flush = function () {
  if ('closed' != this.readyState && this.transport.writable
    && !this.upgrading && this.writeBuffer.length) {
    // debug: flushing %d packets in socket, this.writeBuffer.length
    this.transport.send(this.writeBuffer);
    this.writeBuffer = [];
  }
};

/**
 * Sends a message.
 *
 * @param {String} message.
 * @return {Socket} for chaining.
 * @api public
 */

Socket.prototype.send = function (msg) {
  this.sendPacket('message', msg);
  return this;
};

/**
 * Sends a packet.
 *
 * @param {String} packet type.
 * @param {String} data.
 * @api private
 */

Socket.prototype.sendPacket = function (type, data) {
  var packet = { type: type, data: data };
  this.writeBuffer.push(packet);
  this.flush();
};

/**
 * Closes the connection.
 *
 * @api private
 */

Socket.prototype.close = function () {
  if ('opening' == this.readyState || 'open' == this.readyState) {
    this.onClose('forced close');
    // debug: socket closing - telling transport to close
    this.transport.close();
  }

  return this;
};

/**
 * Called upon transport error
 *
 * @api private
 */

Socket.prototype.onError = function (err) {
  this.emit('error', err);
  this.onClose('transport error', err);
};

/**
 * Called upon transport close.
 *
 * @api private
 */

Socket.prototype.onClose = function (reason, desc) {
  if ('closed' != this.readyState) {
    // debug: socket close: "%s", reason
    this.readyState = 'closed';
    this.emit('close', reason, desc);
    this.onclose && this.onclose.call(this);
  }
};

/**
 * Generates a random uid.
 *
 * @api private
 */

function rnd () {
  return String(Math.random()).substr(5) + String(Math.random()).substr(5);
}
<|endoftext|>


<commit_before>var _ = require("lodash");

var level = 0;
var _logger;

// Just display "useful" information about the configuration
module.exports = function(logger, app, configuration) {
    _logger = logger;
    var splash = app.get("name") + " configuration -------------\n";
    splash += logObject(configuration);
    splash += " ------------------------------------------------\n";
    _logger.info(splash);
};

function logObject(configuration) {
    var result = "";
    level++;

    _.each(configuration, function(value, key) {
        if (_.isObject(value)) {
            if (level === 1) {
                result += blankLine();
            }
            result += logKey(key);
            result += logObject(value);
        } else {
            if (level === 1) {
                result += blankLine();
            }
            result += logValue(key, value);
        }
    });
    level--;
    return result;
}

function logKey(key){
    return "| " +  indent() + " " + key + "\n";
}

function logValue(key, value) {
    return "| " + indent() + " " + key + ": " + value + "\n";
}

function separator() {
    return "| -----------\n";
}

function blankLine() {
    return "|\n";
}

function indent() {
    var nbspaces = level * 2;
    return Array(nbspaces + 1).join("-");
}
<commit_msg>Remove unused 'separator()' function<commit_after>var _ = require("lodash");

var level = 0;
var _logger;

// Just display "useful" information about the configuration
module.exports = function(logger, app, configuration) {
    _logger = logger;
    var splash = app.get("name") + " configuration -------------\n";
    splash += logObject(configuration);
    splash += " ------------------------------------------------\n";
    _logger.info(splash);
};

function logObject(configuration) {
    var result = "";
    level++;

    _.each(configuration, function(value, key) {
        if (_.isObject(value)) {
            if (level === 1) {
                result += blankLine();
            }
            result += logKey(key);
            result += logObject(value);
        } else {
            if (level === 1) {
                result += blankLine();
            }
            result += logValue(key, value);
        }
    });
    level--;
    return result;
}

function logKey(key) {
    return "| " + indent() + " " + key + "\n";
}

function logValue(key, value) {
    return "| " + indent() + " " + key + ": " + value + "\n";
}

function blankLine() {
    return "|\n";
}

function indent() {
    var nbspaces = level * 2;
    return Array(nbspaces + 1).join("-");
}
<|endoftext|>


<commit_before>/**
 * @copyright Maichong Software Ltd. 2016 http://maichong.it
 * @date 2016-04-10
 * @author Liang <liang@maichong.it>
 */

'use strict';

const EventEmitter = require('events');
const Segment = require('./segment');
const Packet = require('./Packet');
const CMD = require('./cmd');

class Stream extends EventEmitter {
  constructor(id, connection) {
    super();
    this.id = id;
    this.connection = connection;
    this.sentMaxSegmentId = 0; //发送方,发送的最大Segment ID
    this.successMaxSegmentId = 0; //发送方,确认发送成功的最大Segment ID
    this.receivedMaxSegmentId = 0; //接收方,已经收到的最大Segment ID
    this.confirmedMaxSegmentId = 0; //接收方,已经确认的最大Segment ID
    this.received = {}; // 接收的 Segment 缓冲
    this.receivedLength = 0;
    this.confirmTimer = 0;//确认Timer
    this.lastAlive = Date.now();
    this.sent = {};//发送端,已经发送,待确认的Segment
    this.sentLength = 0;
    this.bytesRead = 0;
    this.bytesWritten = 0;
    this.remoteAddress = connection.remoteAddress;
    this.remotePort = connection.remotePort;
    this._writeQueue = [];
    this.writeTimer = 0;

    this.aliveTimer = setInterval(() => {
      let now = Date.now();
      if (now - this.lastAlive > 300000) {
        //5分钟不活跃
        this.close();
        return;
      }
      if (this.receivedLength && now - this.lastAlive > 5000) {
        //5秒内不活跃,并且有需要重发的包
        //请求重发
        this._confirm();
        return;
      }
    }, 5000);

    this.resendTimer = setInterval(() => {
      let now = Date.now();
      for (let i in this.sent) {
        let segment = this.sent[i];
        if (now - segment.lastSent > 100) {
          segment.lastSent = now;
          if (segment.a && !segment.a.onBus) {
            this.connection._send(segment.a);
            segment.ra = 0;
          }
          if (segment.b && !segment.b.onBus) {
            this.connection._send(segment.b);
            segment.rb = 0;
          }
          if (segment.c && !segment.c.onBus) {
            this.connection._send(segment.c);
            segment.rc = 0;
          }
          if (segment.p && !segment.p.onBus) {
            this.connection._send(segment.p);
          }
        }
      }
      if (!this.confirmTimer && this.receivedLength) {
        this.confirmTimer = setTimeout(() => {
          this.confirmTimer = 0;
          this._confirm();
        }, 10);
      }
    }, 20);
  }

  close() {
    //console.log('stream', this.id, 'close');
    if (this.connection) {
      this.emit('close');
      let buffer = new Buffer(4);
      buffer.writeUInt8(CMD.CLOSE_STREAM);
      this.idBuffer.copy(buffer, 1);
      let packet = new Packet(buffer);
      this.connection.send(packet);
    }
    delete this._idBuffer;
    delete this.received;
    delete this.sent;
    delete this.connection;
    delete this._writeQueue;
    if (this.confirmTimer) {
      clearTimeout(this.confirmTimer);
    }
    clearInterval(this.aliveTimer);
    clearInterval(this.resendTimer);
  }

  get idBuffer() {
    if (!this._idBuffer) {
      this._idBuffer = Stream.idToBuffer(this.id);
    }
    return this._idBuffer;
  }

  static idToBuffer(id) {
    let tmp = new Buffer(4);
    let buffer = new Buffer(3);
    tmp.writeInt32LE(id, 0);
    tmp.copy(buffer, 0, 0, 3);
    return buffer;
  }

  static bufferToId(buffer) {
    let tmp = new Buffer(4);
    tmp.fill(0, 3);
    buffer.copy(tmp);
    return tmp.readInt32LE(0);
  }

  /**
   * @param data
   * @param [encoding]
   * @param [callback]
   */
  write(data, encoding, callback) {
    if (!data || !data.length) {
      return;
    }
    if (typeof data === 'string') {
      data = new Buffer(data, encoding);
    }

    //如果一次写入的数据大于单个包最大数据
    //拆开写入多次
    if (data.length > 4350) {
      for (let start = 0; start < data.length; start += 4350) {
        let end = start + 4350;
        if (end > data.length) {
          end = data.length;
        }
        this._writeQueue.push(data.slice(start, end));
      }
    } else {
      this.bytesWritten += data.length;
    }
    if (!this.writeTimer) {
      this._write();
    }
  }

  _write() {
    this.writeTimer = 0;
    if (!this._writeQueue.length) {
      return;
    }
    if (this.sentLength > 500) {
      this.writeTimer = setTimeout(() => this._write(), 20);
      return;
    }
    let data = this._writeQueue.shift();
    if (!data) {
      return;
    }
    this.sentMaxSegmentId++;
    let segment = new Segment(this.sentMaxSegmentId, this, data);
    segment.pack();
    segment.lastSent = Date.now();
    this.connection.send(segment.a);
    this.connection.send(segment.b);
    this.connection.send(segment.c);
    //this.connection.send(segment.p);
    this.sent[segment.id] = segment;
    this.sentLength++;
    let now = Date.now();
    if (!this._lastLog || now - this._lastLog > 600) {
      console.log(this._writeQueue.length, this.sentLength);
      this._lastLog = now;
    }
    if (this.sentLength < 200) {
      this._write();
    } else {
      this.writeTimer = setTimeout(() => this._write(), 3);
    }
  }

  /**
   * 当收到Packet
   * @param packet
   */
  receive(packet) {
    let now = this.lastAlive = Date.now();
    if (packet.cmd === CMD.CONFIRM) {
      let segmentId = packet.segmentId;
      for (let i = this.successMaxSegmentId + 1; i <= packet.segmentId; i++) {
        if (this.sent[i]) {
          this.sentLength--;
          delete this.sent[i];
          //console.log('receive.remove', i);
        }
      }
      //console.log('confirm', segmentId - this.successMaxSegmentId);
      this.successMaxSegmentId = segmentId;

      //SACK
      let buffer = packet.buffer;
      let index = 8;
      let lastSegmentId = segmentId;
      while (buffer.length > index) {
        let offset = buffer.readUInt8(index);
        if (!offset) {
          throw new Error('offset ' + offset);
        }
        let code = buffer.readUInt8(index + 1);
        index += 2;
        let id = segmentId + offset;
        let segment = this.sent[id];
        if (!segment) {
          console.error('error , can not resend ', id);
          continue;
        }
        while (id - lastSegmentId > 1) {
          lastSegmentId++;
          //中间有一些已经接收到了
          if (this.sent[lastSegmentId]) {
            delete this.sent[lastSegmentId];
            this.sentLength--;
            //console.log('remove', lastSegmentId);
          }
          //console.log(lastSegmentId, id);
        }
        lastSegmentId = id;

        //console.log('resend', id, 'by offset ', offset, 'to', segmentId);

        if (segment.a && code % CMD.DATA_A === 0) {
          //重发A
          segment.ra++;
          if (segment.ra > 1) {
            this.connection._send(segment.a);
            segment.ra = 0;
            segment.lastSent = now;
          }
          //console.log('%s.ra:%s', id, segment.ra);
        } else {
          //已收到
          delete segment.a;
        }
        if (segment.b && code % CMD.DATA_B === 0) {
          //重发B
          segment.rb++;
          if (segment.rb > 1) {
            this.connection._send(segment.b);
            segment.rb = 0;
            segment.lastSent = now;
          }
          //console.log('%s.rb:%s', id, segment.rb);
        } else {
          //已收到
          delete segment.b;
        }
        if (segment.c && code % CMD.DATA_C === 0) {
          //重发A
          segment.rc++;
          if (segment.rc > 1) {
            this.connection._send(segment.c);
            segment.rc = 0;
            segment.lastSent = now;
          }
          //console.log('%s.rc:%s', id, segment.rc);
        } else {
          //已收到
          delete segment.c;
        }
        if (segment.p && code % CMD.DATA_RESEND_P === 0) {
          //重发P
          segment.rp++;
          if (segment.rp > 1) {
            this.connection._send(segment.p);
            segment.rp = 0;
            segment.lastSent = now;
          }
          //console.log('%s.rp:%s', id, segment.rp);
        } else {
          //已收到
          delete segment.c;
        }
      }
    }
  }

  /**
   * 当收到数据Packet
   * @param packet
   */
  receiveData(packet) {
    this.lastAlive = Date.now();
    let segmentId = packet.segmentId;
    if (segmentId < this.confirmedMaxSegmentId) {
      //已经确认,重复接收
      return;
    }

    if (segmentId - this.confirmedMaxSegmentId > 5000) {
      //segmentId太大,不接受
      return;
    }

    let segment = this.received[segmentId];
    if (!segment) {
      segment = this.received[segmentId] = new Segment(segmentId, this, null);
    }

    let cmd = packet.cmd;
    if (!segment.a && cmd % CMD.DATA_A === 0) {
      //a
      segment.a = packet;
      segment.count++;
    } else if (!segment.b && cmd % CMD.DATA_B === 0) {
      //b
      segment.b = packet;
      segment.count++;
    } else if (!segment.c && cmd % CMD.DATA_C === 0) {
      //c
      segment.c = packet;
      segment.count++;
    } else if (!segment.p && cmd % CMD.DATA_P === 0) {
      //p
      segment.p = packet;
      segment.count++;
    } else {
      //没有能够识别包,或重复接收
      if (!segment.count) {
        delete this.received[segmentId];
      }
      return;
    }

    if (segmentId > this.receivedMaxSegmentId) {
      this.receivedMaxSegmentId = segmentId;
    }
    this.receivedLength = this.receivedMaxSegmentId - this.confirmedMaxSegmentId;

    if (segment.count >= 3) {
      this._readData();
    }
  }

  _readData() {
    let i = this.confirmedMaxSegmentId;
    let buffers = [];
    while (true) {
      let segment = this.received[i + 1];
      if (!segment || segment.count < 3) {
        break;
      }
      try {
        let buffer = segment.unpack();
        buffers.push(buffer);
        i++;
        delete this.received[i];
      } catch (e) {
        break;
      }
    }
    if (!buffers.length) {
      return;
    }
    this.confirmedMaxSegmentId = i;
    this.receivedLength = this.receivedMaxSegmentId - this.confirmedMaxSegmentId;
    let data;
    if (buffers.length === 1) {
      data = buffers[0];
    } else {
      data = Buffer.concat(buffers);
    }
    this.bytesRead += data.length;
    setImmediate(() => {
      this.emit('data', data);
    });
    if (!this.confirmTimer) {
      this.confirmTimer = setTimeout(() => {
        //发送确认包
        this.confirmTimer = 0;
        this._confirm();
      }, 10);
    }
  }

  _confirm() {
    //发送确认包
    let buffer = new Buffer(8);
    buffer.writeUInt8(CMD.CONFIRM, 0);
    this.idBuffer.copy(buffer, 1);
    buffer.writeUInt32LE(this.confirmedMaxSegmentId, 4);

    let buffers = [buffer];
    if (this.receivedMaxSegmentId > this.confirmedMaxSegmentId) {
      //有空白
      for (let offset = 1; offset < 250 && this.confirmedMaxSegmentId + offset <= this.receivedMaxSegmentId; offset++) {
        let i = this.confirmedMaxSegmentId + offset;
        let segment = this.received[i];
        if (segment && segment.count >= 3) {
          continue;
        }
        let sack = new Buffer(2);
        sack.writeUInt8(offset);
        let code = 1;
        if (!segment) {
          code = CMD.DATA_RESEND_ALL;
        } else {
          if (!segment.a) {
            code *= CMD.DATA_A;
          }
          if (!segment.b) {
            code *= CMD.DATA_B;
          }
          if (!segment.c) {
            code *= CMD.DATA_C;
          }
          if (!segment.p) {
            code *= CMD.DATA_RESEND_P;
          }
        }
        sack.writeUInt8(code, 1);
        buffers.push(sack);
      }
    }

    if (buffers.length > 1) {
      buffer = Buffer.concat(buffers);
    }
    let packet = new Packet(buffer);
    this.connection._send(packet);
  }
}

Stream.maxId = 16777216;

module.exports = Stream;
<commit_msg>fix bug<commit_after>/**
 * @copyright Maichong Software Ltd. 2016 http://maichong.it
 * @date 2016-04-10
 * @author Liang <liang@maichong.it>
 */

'use strict';

const EventEmitter = require('events');
const Segment = require('./segment');
const Packet = require('./packet');
const CMD = require('./cmd');

class Stream extends EventEmitter {
  constructor(id, connection) {
    super();
    this.id = id;
    this.connection = connection;
    this.sentMaxSegmentId = 0; //发送方,发送的最大Segment ID
    this.successMaxSegmentId = 0; //发送方,确认发送成功的最大Segment ID
    this.receivedMaxSegmentId = 0; //接收方,已经收到的最大Segment ID
    this.confirmedMaxSegmentId = 0; //接收方,已经确认的最大Segment ID
    this.received = {}; // 接收的 Segment 缓冲
    this.receivedLength = 0;
    this.confirmTimer = 0;//确认Timer
    this.lastAlive = Date.now();
    this.sent = {};//发送端,已经发送,待确认的Segment
    this.sentLength = 0;
    this.bytesRead = 0;
    this.bytesWritten = 0;
    this.remoteAddress = connection.remoteAddress;
    this.remotePort = connection.remotePort;
    this._writeQueue = [];
    this.writeTimer = 0;

    this.aliveTimer = setInterval(() => {
      let now = Date.now();
      if (now - this.lastAlive > 300000) {
        //5分钟不活跃
        this.close();
        return;
      }
      if (this.receivedLength && now - this.lastAlive > 5000) {
        //5秒内不活跃,并且有需要重发的包
        //请求重发
        this._confirm();
        return;
      }
    }, 5000);

    this.resendTimer = setInterval(() => {
      let now = Date.now();
      for (let i in this.sent) {
        let segment = this.sent[i];
        if (now - segment.lastSent > 100) {
          segment.lastSent = now;
          if (segment.a && !segment.a.onBus) {
            this.connection._send(segment.a);
            segment.ra = 0;
          }
          if (segment.b && !segment.b.onBus) {
            this.connection._send(segment.b);
            segment.rb = 0;
          }
          if (segment.c && !segment.c.onBus) {
            this.connection._send(segment.c);
            segment.rc = 0;
          }
          if (segment.p && !segment.p.onBus) {
            this.connection._send(segment.p);
          }
        }
      }
      if (!this.confirmTimer && this.receivedLength) {
        this.confirmTimer = setTimeout(() => {
          this.confirmTimer = 0;
          this._confirm();
        }, 10);
      }
    }, 20);
  }

  close() {
    //console.log('stream', this.id, 'close');
    if (this.connection) {
      this.emit('close');
      let buffer = new Buffer(4);
      buffer.writeUInt8(CMD.CLOSE_STREAM);
      this.idBuffer.copy(buffer, 1);
      let packet = new Packet(buffer);
      this.connection.send(packet);
    }
    delete this._idBuffer;
    delete this.received;
    delete this.sent;
    delete this.connection;
    delete this._writeQueue;
    if (this.confirmTimer) {
      clearTimeout(this.confirmTimer);
    }
    clearInterval(this.aliveTimer);
    clearInterval(this.resendTimer);
  }

  get idBuffer() {
    if (!this._idBuffer) {
      this._idBuffer = Stream.idToBuffer(this.id);
    }
    return this._idBuffer;
  }

  static idToBuffer(id) {
    let tmp = new Buffer(4);
    let buffer = new Buffer(3);
    tmp.writeInt32LE(id, 0);
    tmp.copy(buffer, 0, 0, 3);
    return buffer;
  }

  static bufferToId(buffer) {
    let tmp = new Buffer(4);
    tmp.fill(0, 3);
    buffer.copy(tmp);
    return tmp.readInt32LE(0);
  }

  /**
   * @param data
   * @param [encoding]
   * @param [callback]
   */
  write(data, encoding, callback) {
    if (!data || !data.length) {
      return;
    }
    if (typeof data === 'string') {
      data = new Buffer(data, encoding);
    }

    //如果一次写入的数据大于单个包最大数据
    //拆开写入多次
    if (data.length > 4350) {
      for (let start = 0; start < data.length; start += 4350) {
        let end = start + 4350;
        if (end > data.length) {
          end = data.length;
        }
        this._writeQueue.push(data.slice(start, end));
      }
    } else {
      this.bytesWritten += data.length;
    }
    if (!this.writeTimer) {
      this._write();
    }
  }

  _write() {
    this.writeTimer = 0;
    if (!this._writeQueue.length) {
      return;
    }
    if (this.sentLength > 500) {
      this.writeTimer = setTimeout(() => this._write(), 20);
      return;
    }
    let data = this._writeQueue.shift();
    if (!data) {
      return;
    }
    this.sentMaxSegmentId++;
    let segment = new Segment(this.sentMaxSegmentId, this, data);
    segment.pack();
    segment.lastSent = Date.now();
    this.connection.send(segment.a);
    this.connection.send(segment.b);
    this.connection.send(segment.c);
    //this.connection.send(segment.p);
    this.sent[segment.id] = segment;
    this.sentLength++;
    let now = Date.now();
    if (!this._lastLog || now - this._lastLog > 600) {
      console.log(this._writeQueue.length, this.sentLength);
      this._lastLog = now;
    }
    if (this.sentLength < 200) {
      this._write();
    } else {
      this.writeTimer = setTimeout(() => this._write(), 3);
    }
  }

  /**
   * 当收到Packet
   * @param packet
   */
  receive(packet) {
    let now = this.lastAlive = Date.now();
    if (packet.cmd === CMD.CONFIRM) {
      let segmentId = packet.segmentId;
      for (let i = this.successMaxSegmentId + 1; i <= packet.segmentId; i++) {
        if (this.sent[i]) {
          this.sentLength--;
          delete this.sent[i];
          //console.log('receive.remove', i);
        }
      }
      //console.log('confirm', segmentId - this.successMaxSegmentId);
      this.successMaxSegmentId = segmentId;

      //SACK
      let buffer = packet.buffer;
      let index = 8;
      let lastSegmentId = segmentId;
      while (buffer.length > index) {
        let offset = buffer.readUInt8(index);
        if (!offset) {
          throw new Error('offset ' + offset);
        }
        let code = buffer.readUInt8(index + 1);
        index += 2;
        let id = segmentId + offset;
        let segment = this.sent[id];
        if (!segment) {
          console.error('error , can not resend ', id);
          continue;
        }
        while (id - lastSegmentId > 1) {
          lastSegmentId++;
          //中间有一些已经接收到了
          if (this.sent[lastSegmentId]) {
            delete this.sent[lastSegmentId];
            this.sentLength--;
            //console.log('remove', lastSegmentId);
          }
          //console.log(lastSegmentId, id);
        }
        lastSegmentId = id;

        //console.log('resend', id, 'by offset ', offset, 'to', segmentId);

        if (segment.a && code % CMD.DATA_A === 0) {
          //重发A
          segment.ra++;
          if (segment.ra > 1) {
            this.connection._send(segment.a);
            segment.ra = 0;
            segment.lastSent = now;
          }
          //console.log('%s.ra:%s', id, segment.ra);
        } else {
          //已收到
          delete segment.a;
        }
        if (segment.b && code % CMD.DATA_B === 0) {
          //重发B
          segment.rb++;
          if (segment.rb > 1) {
            this.connection._send(segment.b);
            segment.rb = 0;
            segment.lastSent = now;
          }
          //console.log('%s.rb:%s', id, segment.rb);
        } else {
          //已收到
          delete segment.b;
        }
        if (segment.c && code % CMD.DATA_C === 0) {
          //重发A
          segment.rc++;
          if (segment.rc > 1) {
            this.connection._send(segment.c);
            segment.rc = 0;
            segment.lastSent = now;
          }
          //console.log('%s.rc:%s', id, segment.rc);
        } else {
          //已收到
          delete segment.c;
        }
        if (segment.p && code % CMD.DATA_RESEND_P === 0) {
          //重发P
          segment.rp++;
          if (segment.rp > 1) {
            this.connection._send(segment.p);
            segment.rp = 0;
            segment.lastSent = now;
          }
          //console.log('%s.rp:%s', id, segment.rp);
        } else {
          //已收到
          delete segment.c;
        }
      }
    }
  }

  /**
   * 当收到数据Packet
   * @param packet
   */
  receiveData(packet) {
    this.lastAlive = Date.now();
    let segmentId = packet.segmentId;
    if (segmentId < this.confirmedMaxSegmentId) {
      //已经确认,重复接收
      return;
    }

    if (segmentId - this.confirmedMaxSegmentId > 5000) {
      //segmentId太大,不接受
      return;
    }

    let segment = this.received[segmentId];
    if (!segment) {
      segment = this.received[segmentId] = new Segment(segmentId, this, null);
    }

    let cmd = packet.cmd;
    if (!segment.a && cmd % CMD.DATA_A === 0) {
      //a
      segment.a = packet;
      segment.count++;
    } else if (!segment.b && cmd % CMD.DATA_B === 0) {
      //b
      segment.b = packet;
      segment.count++;
    } else if (!segment.c && cmd % CMD.DATA_C === 0) {
      //c
      segment.c = packet;
      segment.count++;
    } else if (!segment.p && cmd % CMD.DATA_P === 0) {
      //p
      segment.p = packet;
      segment.count++;
    } else {
      //没有能够识别包,或重复接收
      if (!segment.count) {
        delete this.received[segmentId];
      }
      return;
    }

    if (segmentId > this.receivedMaxSegmentId) {
      this.receivedMaxSegmentId = segmentId;
    }
    this.receivedLength = this.receivedMaxSegmentId - this.confirmedMaxSegmentId;

    if (segment.count >= 3) {
      this._readData();
    }
  }

  _readData() {
    let i = this.confirmedMaxSegmentId;
    let buffers = [];
    while (true) {
      let segment = this.received[i + 1];
      if (!segment || segment.count < 3) {
        break;
      }
      try {
        let buffer = segment.unpack();
        buffers.push(buffer);
        i++;
        delete this.received[i];
      } catch (e) {
        break;
      }
    }
    if (!buffers.length) {
      return;
    }
    this.confirmedMaxSegmentId = i;
    this.receivedLength = this.receivedMaxSegmentId - this.confirmedMaxSegmentId;
    let data;
    if (buffers.length === 1) {
      data = buffers[0];
    } else {
      data = Buffer.concat(buffers);
    }
    this.bytesRead += data.length;
    setImmediate(() => {
      this.emit('data', data);
    });
    if (!this.confirmTimer) {
      this.confirmTimer = setTimeout(() => {
        //发送确认包
        this.confirmTimer = 0;
        this._confirm();
      }, 10);
    }
  }

  _confirm() {
    //发送确认包
    let buffer = new Buffer(8);
    buffer.writeUInt8(CMD.CONFIRM, 0);
    this.idBuffer.copy(buffer, 1);
    buffer.writeUInt32LE(this.confirmedMaxSegmentId, 4);

    let buffers = [buffer];
    if (this.receivedMaxSegmentId > this.confirmedMaxSegmentId) {
      //有空白
      for (let offset = 1; offset < 250 && this.confirmedMaxSegmentId + offset <= this.receivedMaxSegmentId; offset++) {
        let i = this.confirmedMaxSegmentId + offset;
        let segment = this.received[i];
        if (segment && segment.count >= 3) {
          continue;
        }
        let sack = new Buffer(2);
        sack.writeUInt8(offset);
        let code = 1;
        if (!segment) {
          code = CMD.DATA_RESEND_ALL;
        } else {
          if (!segment.a) {
            code *= CMD.DATA_A;
          }
          if (!segment.b) {
            code *= CMD.DATA_B;
          }
          if (!segment.c) {
            code *= CMD.DATA_C;
          }
          if (!segment.p) {
            code *= CMD.DATA_RESEND_P;
          }
        }
        sack.writeUInt8(code, 1);
        buffers.push(sack);
      }
    }

    if (buffers.length > 1) {
      buffer = Buffer.concat(buffers);
    }
    let packet = new Packet(buffer);
    this.connection._send(packet);
  }
}

Stream.maxId = 16777216;

module.exports = Stream;
<|endoftext|>


<commit_before>var _ = require('lodash');
// var Boom = require('boom');

var stripe = {};

stripe.customersCreateOrNot = function( context ) {
	if ( context.userDoc.stripe && context.userDoc.stripe.customerId ) {
		return;
	}

	var params = {
		description: 'Customer for ' + context.userDoc.name.split('/')[1],
		metadata: { 'hoodieId': context.userDoc.id },
	};

	if ( /^[\S]+@[\S]+$/.test( context.userDoc.id ) ) {
		params.email = context.userDoc.id;
	}

	return context.stripe.customers.create( params )
		.then(function( customer ) {
			context.userNeedsUpdate = true;
			context.customer = customer;

			if ( !context.userDoc.stripe ) {
				context.userDoc.stripe = {};
			}

			context.userDoc.stripe.customerId = customer.id;
			context.userDoc.stripe.plan = 'free_none';
		});
};

stripe.customersRetrieveOrNot = function( context ) {
	if ( context.customer ) {
		return;
	}

	return context.stripe.customers.retrieve(
			context.userDoc.stripe.customerId
		)
		.then(function( customer ) {
			context.customer = customer;
		});
};

stripe.chargesListOrNot = function( context ) {
	if (
		!context.customer ||
		!context.args ||
		!context.args[0] ||
		context.args[0].includeCharges !== true
	) {
		return;
	}

	return context.stripe.charges.list({
			customer: context.customer.id,
			limit: 12,
		})
		.then(function( charges ) {
			context.customer.charges = charges;
		});
};

stripe.invoicesRetrieveUpcoming = function( context ) {
	var customer = context.userDoc.stripe;
	var whitelist = [
			'subscription_plan',
			'subscription_quantity',
			'subscription_trial_end',
		];
	// mix following object with whitelisted properties from requestData
	var params = _.assign({
			'subscription_prorate': true,
		},
		_.pick(context.args[0], function( value, key ) {
			return _.includes( whitelist, key );
		}));

	return context.stripe.invoices.retrieveUpcoming.apply(
			context.stripe.invoices,
			customer.subscriptionId ? [
				customer.customerId,
				customer.subscriptionId,
				params,
			] : [
				customer.customerId,
				params,
			]
		);
};

stripe.tokensRetrieveOrNot = function( context ) {
	var source = context.args && context.args[0] && context.args[0].source;

	if ( !source ) {
		return;
	}

	return context.stripe.tokens.retrieve( source )
		.then(function( token ) {
			context.token = token;
		});
};

stripe.customersCreateOrUpdateOrNot = function( context ) {
	var userDoc = context.userDoc;
	var taxamo = context.userDoc.taxamo;
	var config = context.hoodie.config;
	var isUpdate = context.userDoc.stripe && context.userDoc.stripe.customerId;

	context.customer = {};

	var createWhitelist = [
			'source',
			'coupon',
			'quantity',
			'email',
			'plan',
		];
	var updateWhitelist = [
			'source',
			'coupon',
			'quantity',
			'email',
		];

	// mix following object with whitelisted properties from requestData
	var params = _.pick(context.args[0], function( value, key ) {
		return _.includes(
			isUpdate ? updateWhitelist : createWhitelist,
			key
		);
	});

	if ( !isUpdate ) {
		params.description = 'Customer for ' + userDoc.name.split('/')[1];
		params.metadata = { 'hoodieId': userDoc.id };
	}

	if ( context.taxamo ) {
		params.metadata = {
			'hoodieId': userDoc.id,
			'taxamo_transaction_key': context.taxamo.key,
		};
	}

	if ( Object.keys( params ).length === 0 ) {
		return;
	}

	// the userDoc.id might be the user's email
	if ( !isUpdate && !params.email && /^[\S]+@[\S]+$/.test(userDoc.id) ) {
		params.email = userDoc.id;
	}

	// TODO: there should be a way to configure the tax-rate globally
	if ( params.plan ) {
		params['tax_percent'] = !taxamo || config.get('universalPricing') ?
			0 :
			taxamo['tax_rate'];
	}

	return context.stripe.customers[ isUpdate ? 'update' : 'create' ].apply(
			context.stripe.customers,
			isUpdate ? [
				userDoc.stripe.customerId,
				params,
			] : [
				params,
			]
		)
		.then(function(body) {
			context.userNeedsUpdate = true;
			context.customer = body;

			if ( !userDoc.stripe ) {
				userDoc.stripe = {};
			}

			userDoc.stripe.customerId = body.id;
			userDoc.stripe.plan = 'free_none';
			if (
				body.subscriptions &&
				body.subscriptions.data &&
				body.subscriptions.data.length
			) {
				userDoc.stripe.subscriptionId = body.subscriptions.data[0].id;
				userDoc.stripe.plan = body.subscriptions.data[0].plan.id;
			}
		});
};

stripe.customersCreateOrUpdateOrNotSubscription = function( context ) {
	var customer = context.userDoc.stripe;
	var taxamo = context.userDoc.taxamo;
	var config = context.hoodie.config;

	var params = { plan: context.args[0].plan };

	// TODO: there should be a way to configure the tax-rate globally
	if ( params.plan ) {
		params['tax_percent'] = !taxamo || config.get('universalPricing') ?
			0 :
			taxamo['tax_rate'];
	}

	var method;
	if ( !params.plan && context.method === 'customers.updateSubscription' ) {
		method = 'cancel' + 'Subscription';
	}
	else if (
		params.plan &&
		customer.subscriptionId &&
		params.plan != context.userDoc.stripe.plan
	) {
		method = 'update' + 'Subscription';
	}
	else if ( params.plan && !customer.subscriptionId ) {
		method = 'create' + 'Subscription';
	}
	else {
		return;
	}

	return context.stripe.customers[ method ].apply(
			context.stripe.customers,
			method === 'createSubscription' ? [
				customer.customerId,
				params,
			] : method === 'updateSubscription' ? [
				customer.customerId,
				customer.subscriptionId,
				params,
			] : [
				customer.customerId,
			]
		)
		.then(function( body ) {
			context.userNeedsUpdate = true;

			customer.subscriptionId = body.status === 'canceled' ?
				undefined :
				body.id;
			customer.plan = body.status === 'canceled' ?
				'free_none' :
				body.plan.id;
		});
};

stripe.orderCreateOrNot = function( context ) {
	if ( !context.args[0]['items'] ) {
		return;
	}

	var customer = context.userDoc.stripe;
	var whitelist = [
		'coupon',
	];
	var defaults = {
		currency: context.args[0]['currency_code'],
		customer: customer.customerId,
		items: context.args[0]['items']
	};

	// mix following object with whitelisted properties from requestData
	var params = _.pick(context.args[0], function( value, key ) {
		return _.includes(whitelist, key);
	});

	params = Object.assign(params, defaults);

	return context.stripe.orders.create(params)
		.then(function(order) {
			context.order = order;
		});
};

stripe.orderPayOrNot = function( context ) {
	if ( !context.order ) {
		return;
	}

	return context.stripe.orders.pay(
			context.order.id,
			context.source ? { source: context.source } : undefined
		)
		.then(function(order) {
			context.order = order;
		});
};

stripe.customerCreditsUpdateOrNot = function( context ) {
	if ( !('credits' in context) ) {
		return;
	}

	context.stripe.customers.update(context.customer, {
		metadata: { credits: context.credits }
	});
}

module.exports = stripe;
<commit_msg>fix orders.create parameters<commit_after>var _ = require('lodash');
// var Boom = require('boom');

var stripe = {};

stripe.customersCreateOrNot = function( context ) {
	if ( context.userDoc.stripe && context.userDoc.stripe.customerId ) {
		return;
	}

	var params = {
		description: 'Customer for ' + context.userDoc.name.split('/')[1],
		metadata: { 'hoodieId': context.userDoc.id },
	};

	if ( /^[\S]+@[\S]+$/.test( context.userDoc.id ) ) {
		params.email = context.userDoc.id;
	}

	return context.stripe.customers.create( params )
		.then(function( customer ) {
			context.userNeedsUpdate = true;
			context.customer = customer;

			if ( !context.userDoc.stripe ) {
				context.userDoc.stripe = {};
			}

			context.userDoc.stripe.customerId = customer.id;
			context.userDoc.stripe.plan = 'free_none';
		});
};

stripe.customersRetrieveOrNot = function( context ) {
	if ( context.customer ) {
		return;
	}

	return context.stripe.customers.retrieve(
			context.userDoc.stripe.customerId
		)
		.then(function( customer ) {
			context.customer = customer;
		});
};

stripe.chargesListOrNot = function( context ) {
	if (
		!context.customer ||
		!context.args ||
		!context.args[0] ||
		context.args[0].includeCharges !== true
	) {
		return;
	}

	return context.stripe.charges.list({
			customer: context.customer.id,
			limit: 12,
		})
		.then(function( charges ) {
			context.customer.charges = charges;
		});
};

stripe.invoicesRetrieveUpcoming = function( context ) {
	var customer = context.userDoc.stripe;
	var whitelist = [
			'subscription_plan',
			'subscription_quantity',
			'subscription_trial_end',
		];
	// mix following object with whitelisted properties from requestData
	var params = _.assign({
			'subscription_prorate': true,
		},
		_.pick(context.args[0], function( value, key ) {
			return _.includes( whitelist, key );
		}));

	return context.stripe.invoices.retrieveUpcoming.apply(
			context.stripe.invoices,
			customer.subscriptionId ? [
				customer.customerId,
				customer.subscriptionId,
				params,
			] : [
				customer.customerId,
				params,
			]
		);
};

stripe.tokensRetrieveOrNot = function( context ) {
	var source = context.args && context.args[0] && context.args[0].source;

	if ( !source ) {
		return;
	}

	return context.stripe.tokens.retrieve( source )
		.then(function( token ) {
			context.token = token;
		});
};

stripe.customersCreateOrUpdateOrNot = function( context ) {
	var userDoc = context.userDoc;
	var taxamo = context.userDoc.taxamo;
	var config = context.hoodie.config;
	var isUpdate = context.userDoc.stripe && context.userDoc.stripe.customerId;

	context.customer = {};

	var createWhitelist = [
			'source',
			'coupon',
			'quantity',
			'email',
			'plan',
		];
	var updateWhitelist = [
			'source',
			'coupon',
			'quantity',
			'email',
		];

	// mix following object with whitelisted properties from requestData
	var params = _.pick(context.args[0], function( value, key ) {
		return _.includes(
			isUpdate ? updateWhitelist : createWhitelist,
			key
		);
	});

	if ( !isUpdate ) {
		params.description = 'Customer for ' + userDoc.name.split('/')[1];
		params.metadata = { 'hoodieId': userDoc.id };
	}

	if ( context.taxamo ) {
		params.metadata = {
			'hoodieId': userDoc.id,
			'taxamo_transaction_key': context.taxamo.key,
		};
	}

	if ( Object.keys( params ).length === 0 ) {
		return;
	}

	// the userDoc.id might be the user's email
	if ( !isUpdate && !params.email && /^[\S]+@[\S]+$/.test(userDoc.id) ) {
		params.email = userDoc.id;
	}

	// TODO: there should be a way to configure the tax-rate globally
	if ( params.plan ) {
		params['tax_percent'] = !taxamo || config.get('universalPricing') ?
			0 :
			taxamo['tax_rate'];
	}

	return context.stripe.customers[ isUpdate ? 'update' : 'create' ].apply(
			context.stripe.customers,
			isUpdate ? [
				userDoc.stripe.customerId,
				params,
			] : [
				params,
			]
		)
		.then(function(body) {
			context.userNeedsUpdate = true;
			context.customer = body;

			if ( !userDoc.stripe ) {
				userDoc.stripe = {};
			}

			userDoc.stripe.customerId = body.id;
			userDoc.stripe.plan = 'free_none';
			if (
				body.subscriptions &&
				body.subscriptions.data &&
				body.subscriptions.data.length
			) {
				userDoc.stripe.subscriptionId = body.subscriptions.data[0].id;
				userDoc.stripe.plan = body.subscriptions.data[0].plan.id;
			}
		});
};

stripe.customersCreateOrUpdateOrNotSubscription = function( context ) {
	var customer = context.userDoc.stripe;
	var taxamo = context.userDoc.taxamo;
	var config = context.hoodie.config;

	var params = { plan: context.args[0].plan };

	// TODO: there should be a way to configure the tax-rate globally
	if ( params.plan ) {
		params['tax_percent'] = !taxamo || config.get('universalPricing') ?
			0 :
			taxamo['tax_rate'];
	}

	var method;
	if ( !params.plan && context.method === 'customers.updateSubscription' ) {
		method = 'cancel' + 'Subscription';
	}
	else if (
		params.plan &&
		customer.subscriptionId &&
		params.plan != context.userDoc.stripe.plan
	) {
		method = 'update' + 'Subscription';
	}
	else if ( params.plan && !customer.subscriptionId ) {
		method = 'create' + 'Subscription';
	}
	else {
		return;
	}

	return context.stripe.customers[ method ].apply(
			context.stripe.customers,
			method === 'createSubscription' ? [
				customer.customerId,
				params,
			] : method === 'updateSubscription' ? [
				customer.customerId,
				customer.subscriptionId,
				params,
			] : [
				customer.customerId,
			]
		)
		.then(function( body ) {
			context.userNeedsUpdate = true;

			customer.subscriptionId = body.status === 'canceled' ?
				undefined :
				body.id;
			customer.plan = body.status === 'canceled' ?
				'free_none' :
				body.plan.id;
		});
};

stripe.orderCreateOrNot = function( context ) {
	if ( !context.args[0]['items'] ) {
		return;
	}

	var customer = context.userDoc.stripe;
	var whitelist = [
		'coupon',
	];
	var defaults = {
		currency: context.args[0]['currency_code'],
		customer: customer.customerId,
		items: context.args[0]['items']
	};

	// mix following object with whitelisted properties from requestData
	var params = _.pick(context.args[0], function( value, key ) {
		return _.includes(whitelist, key);
	});

	params = Object.assign(params, defaults);

	return context.stripe.orders.create(params)
		.then(function(order) {
			context.order = order;
		});
};

stripe.orderPayOrNot = function( context ) {
	if ( !context.order ) {
		return;
	}

	return context.stripe.orders.pay(
			context.order.id,
			context.source ? { source: context.source } : {}
		)
		.then(function(order) {
			context.order = order;
		});
};

stripe.customerCreditsUpdateOrNot = function( context ) {
	if ( !('credits' in context) ) {
		return;
	}

	context.stripe.customers.update(context.customer, {
		metadata: { credits: context.credits }
	});
}

module.exports = stripe;
<|endoftext|>


<commit_before>var dgram = require('dgram')
var http = require('http')
var io = require('socket.io')
var chain = require('chain-gang')
var models = require('./models')

var db = process.db

var severityLookup = {
  0: 'emerg',
  1: 'alert',
  2: 'crit',
  3: 'err',
  4: 'warn',
  5: 'notice',
  6: 'info',
  7: 'debug',
}

var facilityLookup = {
  0: 'kern',
  1: 'user',
  2: 'mail',
  3: 'daemon',
  4: 'auth',
  5: 'syslog',
  6: 'lpr',
  7: 'news',
  8: 'uucp',
  9: 'cron',
  10: 'authpriv',
  11: 'ftp',
  12: 'ntp',
  13: 'audit',
  14: 'alert',
  15: 'at',
  16: 'local0',
  17: 'local1',
  18: 'local2',
  19: 'local3',
  20: 'local4',
  21: 'local5',
  22: 'local6',
  23: 'local7',
}

var loggly = function(msg, token) {
  return function(worker) {
    data = JSON.stringify(msg)
    var opts = {
      host: 'logs.loggly.com',
      path: '/inputs/' + token,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': data.length
      }
    }
    var req = http.request(opts)
    req.write(data)
    req.end()
    worker.finish()
  }
}

var logglyPatterns = []
process.nextTick(function setPatterns() {
  var patterns = []
  var streams = db.find('streamForwarding', true)
  for (var s in streams) {
    if (streams[s].streamTerms.length > 0) {
      var pattern = '^'
      var negative = []
      for (var t in streams[s].streamTerms) {
        var term = streams[s].streamTerms[t]
        if (term.substr(0,1) == '!') {
          negative.push(term.slice(1))
        } else {
          pattern += '(?=.*' + term + ')'
        }
      }
      if (negative.length > 0) {
        pattern += '((?!' + negative.join('|') + ').)*$'
      } else {
        pattern += '.*'
      }
      patterns.push({regex: new RegExp(pattern, 'i'), token: streams[s].streamLogglyToken})
    }
  }
  console.log(patterns)
  logglyPatterns = patterns
  setTimeout(setPatterns, 8000)
})

var websocket = io.listen(8000)

var queue = chain.create({workers: 12})

var syslog = dgram.createSocket('udp4')
syslog.on('message', function(msg_orig, rinfo) {
  var msg = (/<([^>]+)>([A-Z][a-z]+\s+\d+\s\d+:\d+:\d+) ([^\s]+) (.*)/).exec(msg_orig)
  if (msg) {
    var facility = Math.floor(msg[1] / 8)
    var msg_json = {
      date: msg[2],
      host: msg[3],
      severity: severityLookup[msg[1] - (facility * 8)],
      facility: facilityLookup[facility],
      message: msg[4]
    }
    websocket.sockets.volatile.emit('msg', msg_json)
    for (var p in logglyPatterns) {
      if (logglyPatterns[p].regex.test(msg[4])) {
        queue.add(loggly(msg_json, logglyPatterns[p].token))
        break
      }
    }
  }
})

exports.server = syslog
<commit_msg>dont need console.log<commit_after>var dgram = require('dgram')
var http = require('http')
var io = require('socket.io')
var chain = require('chain-gang')
var models = require('./models')

var db = process.db

var severityLookup = {
  0: 'emerg',
  1: 'alert',
  2: 'crit',
  3: 'err',
  4: 'warn',
  5: 'notice',
  6: 'info',
  7: 'debug',
}

var facilityLookup = {
  0: 'kern',
  1: 'user',
  2: 'mail',
  3: 'daemon',
  4: 'auth',
  5: 'syslog',
  6: 'lpr',
  7: 'news',
  8: 'uucp',
  9: 'cron',
  10: 'authpriv',
  11: 'ftp',
  12: 'ntp',
  13: 'audit',
  14: 'alert',
  15: 'at',
  16: 'local0',
  17: 'local1',
  18: 'local2',
  19: 'local3',
  20: 'local4',
  21: 'local5',
  22: 'local6',
  23: 'local7',
}

var loggly = function(msg, token) {
  return function(worker) {
    data = JSON.stringify(msg)
    var opts = {
      host: 'logs.loggly.com',
      path: '/inputs/' + token,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': data.length
      }
    }
    var req = http.request(opts)
    req.write(data)
    req.end()
    worker.finish()
  }
}

var logglyPatterns = []
process.nextTick(function setPatterns() {
  var patterns = []
  var streams = db.find('streamForwarding', true)
  for (var s in streams) {
    if (streams[s].streamTerms.length > 0) {
      var pattern = '^'
      var negative = []
      for (var t in streams[s].streamTerms) {
        var term = streams[s].streamTerms[t]
        if (term.substr(0,1) == '!') {
          negative.push(term.slice(1))
        } else {
          pattern += '(?=.*' + term + ')'
        }
      }
      if (negative.length > 0) {
        pattern += '((?!' + negative.join('|') + ').)*$'
      } else {
        pattern += '.*'
      }
      patterns.push({regex: new RegExp(pattern, 'i'), token: streams[s].streamLogglyToken})
    }
  }
  logglyPatterns = patterns
  setTimeout(setPatterns, 8000)
})

var websocket = io.listen(8000)

var queue = chain.create({workers: 12})

var syslog = dgram.createSocket('udp4')
syslog.on('message', function(msg_orig, rinfo) {
  var msg = (/<([^>]+)>([A-Z][a-z]+\s+\d+\s\d+:\d+:\d+) ([^\s]+) (.*)/).exec(msg_orig)
  if (msg) {
    var facility = Math.floor(msg[1] / 8)
    var msg_json = {
      date: msg[2],
      host: msg[3],
      severity: severityLookup[msg[1] - (facility * 8)],
      facility: facilityLookup[facility],
      message: msg[4]
    }
    websocket.sockets.volatile.emit('msg', msg_json)
    for (var p in logglyPatterns) {
      if (logglyPatterns[p].regex.test(msg[4])) {
        queue.add(loggly(msg_json, logglyPatterns[p].token))
        break
      }
    }
  }
})

exports.server = syslog
<|endoftext|>


<commit_before>// Decent Multi-Tenancy (c) 2014-2015 Bertrand Le Roy, under MIT. See LICENSE.txt for licensing details.
'use strict';

// TODO: Add configurable timeout to request handling.
// TODO: Enable tenants to be restarted.
// TODO: add opt-out for debug mode.
// TODO: log all errors instead of sending them to the console.
// TODO: emit errors up onto the tenant.
// TODO: add logger as an option, fallback to console.

let EventEmitter = require('events').EventEmitter;
let util = require('util');
let path = require('path');
let fs = require('fs');
let http = require('http');
let https = require('https');
let domain = require('domain');

/**
 * A tenant is an independant site in the system.
 */
class Tenant extends EventEmitter {
  /**
  * One should usually refrain from using this constructor directly, and do a `Tenant.load` instead.
  * @param {Object} options
  * @param {String} [options.name] The technical name of the tenant. Default is "default".
  * @param {String} [options.displayName] The display name of the tenant. Default is `name`.
  * @param {String} [options.host] The host name under which the tenant answers. If not specified, this is process.env.IP, or `localhost`.
  * @param {String} [options.debugHost] A host name under which the tenant answers, that causes `request.debug` to be true.
  * @param {Number} [options.port] The port to which the tenant answers. Defaults to `process.env.port`, or 80 if that's not found.
  * @param {Number} [options.debugPort] The port associated with the debug host. Defaults to `process.env.port`, or 1337 if that's not found.
  * @param {Boolean} [options.https] True if the site must use HTTPS. False by default.
  * @param {Boolean} [options.debugHttps] True if the site must use HTTPS in debug mode. False by default.
  * @param {String} [options.cert] The path to the SSL certificate to use with this tenant.
  * @param {String} [options.key] The path to the SSL key to use with this tenant.
  * @param {String} [options.pfx] The path to the pfx SSL certificate to use with this tenant.
  * @param {String} [options.debugCert] The path to the SSL certificate to use with this tenant in debug mode. Defaults to `cert`.
  * @param {String} [options.debugKey] The path to the SSL key to use with this tenant in debug mode. Defaults to `key`.
  * @param {String} [options.debugPfx] The path to the pfx SSL certificate to use with this tenant in debug mode. Defaults to `pfx`.
  * @param {Boolean} [options.active] True if the tenant is active.
  * @param {Object} [options.logger] A logger object with a log(level:string, message:string) method. If not specified, log messages go to the console.
  */
  constructor(options) {
    super();
    Object.assign(this, {
      settings: options || {},
      name: options.name || 'default',
      displayName: options.displayName || options.name,
      host: Array.isArray(options.host)
        ? options.host
        : (options.host
          ? [options.host]
          : (process.env.IP
            ? [process.env.IP]
            : ['localhost'])),
      debugHost: Array.isArray(options.debugHost)
        ? options.debugHost
        : (options.debugHost ? [options.debugHost] : ['localhost']),
      port: options.port || process.env.PORT || 80,
      debugPort: options.debugPort || process.env.PORT || 1337,
      https: !!options.https,
      debugHttps: !!options.debugHttps,
      cert: options.cert,
      key: options.key,
      pfx: options.pfx,
      debugCert: options.debugCert || options.cert,
      debugKey: options.debugKey || options.key,
      debugPfx: options.debugPfx || options.pfx,
      features: options.features || {},
      active: !(options.active === false),
      log: (options.logger ? options.logger.log : null) ||
        function log() {
          console.log.apply(null, arguments);
        }
    });
  }

  /**
   * Creates a tenant from a settings file or object, and adds it to `Tenant.list` and `Tenant.names`.
   * @param {String|Object} settingsh The path of the settings file, or a settings object.
   * @param {Object} [defaults] Default settings.
   * @returns {Tenant} The new tenant.
   */
  static load(settings, defaults) {
    defaults = defaults || {};
    if (typeof settings == 'string') {
      settings = require(settings);
    }
    for (let settingName of Object.getOwnPropertyNames(defaults)) {
      if (!settings.hasOwnProperty(settingName)) {
        settings[settingName] = defaults[settingName];
      }
    }
    let tenant = new Tenant(settings);
    Tenant.list[tenant.name] = tenant;
    Tenant.names.push(tenant.name);
    return tenant;
  };

  /**
   * Discovers all tenants in the rootPath directory.
   * This method scans the directory for subdirectories with `settings.json` files
   * in them. Those files are then loaded as site settings.
   * @param {Object} defaults The default settings for the shells.
   * @param {String} rootPath The root path where to look for shell settings files. Defaults to "./sites".
   */
  static discover(defaults, rootPath) {
    rootPath = rootPath || './sites';
    let siteNames = fs.readdirSync(rootPath);
    for (let siteName of siteNames) {
      if (siteName[0] === '.') return;
      let resolvedSitePath = path.resolve(rootPath, siteName, 'settings.json');
      let settings;
      try {
        settings = require(resolvedSitePath);
      }
      catch(ex) {
        ex.path = resolvedSitePath;
        ex.message = 'Failed to load site settings for ' + siteName;
        throw ex;
      }
      settings.settingsPath = resolvedSitePath;
      settings.rootPath = resolvedSitePath;
      settings.name = settings.name || siteName;
      Tenant.load(settings, defaults);
    }
  };

  /**
   * Returns the tenant that should handle this request.
   * @param {IncomingMessage} request The request.
   * @returns {Tenant} The tenant that should handle the request, null if no active tenant is fit to do it.
   */
  static resolve(request) {
    // If there's only one shell, always return that.
    if (Tenant.names.length === 1) return Tenant.list[Tenant.names[0]];
    // Otherwise let each shell decide if it can handle the request.
    for (let shellName of Tenant.names) {
      let shell = Tenant.list[shellName];

      if (shell.active && shell.canHandle(request)) {
        return shell;
      }
    }
    // Unresolved requests should not go to a default shell
    // if there's more than one.
    return null;
  };

  /**
   * Starts http listeners covering for all active tenants in `Tenant.list`.
    let httpServer;
   */
  static listen() {
    let httpServer;
    let port = process.env.PORT || 1337;

    // If iisnode, only create one server.
    // TODO: properly handle https on IIS.
    if (process.env.IISNODE_VERSION && port.substr(0, 9) === '\\\\.\\pipe\\') {
      httpServer = new http.Server();
      httpServer.on('request', localHandler(httpServer));
      httpServer.listen(port);
      return;
    }
    // Listen for each tenant
    for (let shellName of Tenant.names) {
      let tenant = Tenant.list[shellName];
      if (!tenant.active) continue;
      let hosts = Array.isArray(tenant.host) ? tenant.host : [tenant.host];
      for (let host of hosts) {
        let currentPort = tenant.port !== '*' ? tenant.port : port;
        // TODO: create debug servers as needed.
        httpServer = tenant.https
            ? https.createServer({
              host: host,
              key: tenant.key,
              cert: tenant.cert,
              pfx: tenant.pfx
            })
            : new http.Server();
        httpServer.on('request', localHandler(httpServer));
        httpServer.listen(currentPort);
        tenant.log('info', `${tenant.name} started listening on port ${currentPort}.`)
      }
    }
  }

  /**
   * Determines if the tenant can handle that request.
   * @param request The request
   * @returns {Boolean} True if the tenant can handle the request.
   */
  canHandle(request) {
    let host = request.headers.host;
    let hosts = this.host.concat(this.debugHost);
    for (let i = 0; i < hosts.length; i++) {
      let thisHost = hosts[i];
      if ((
        (
        ((this.https && this.port === 443) || this.port === 80 || this.port === '*')
        && thisHost === host
        )
        || (this.port === '*' && host.startsWith(thisHost))
        || (thisHost + ':' + this.port === host)
      ) && (
      !this.path
      || request.url.startsWith(this.path)
      )) {
        if (i >= this.host.length) {
          request.debug = true;
          // We also set the tenant into debug mode, which could cause problems
          // if the same tenant is on a server configured and able to answer on both
          // the host and debug host. That should however never happen.
          this.debug = request.debug;
        }
        return true;
      }
    }
    return false;
  };

  /**
   * Enables or disables the tenant.
   * @param {Boolean} state If provided, sets the enabled state of the tenant. Otherwise, enables the tenant.
   */
  enable(state) {
    this.active = typeof state === 'undefined' || !!state;
  };

  /**
   * Middleware for use, for example, with Express.
   * @param {http.IncomingMessage} request Request
   * @param {http.ServerResponse} response Response
   * @param {Function} next The callback.
   */
  middleware(request, response, next) {
    if (!this.canHandle(request)) {
      next();
      return;
    }
    this.handleRequest(request, response, next);
  };

  /**
   * Handles the request for the tenant.
   * Emits the following events:
   * * decent.multi-tenancy.start-request {tenant, request, response}
   * * decent.multi-tenancy.handle-request {tenant, request, response}
   * * decent.multi-tenancy.end-request {tenant, request, response}
   * * error
   * @param request Request
   * @param response Response
   * @param {Function} next The callback
   */
  handleRequest(request, response, next) {
    let self = this;
    let log = this.log;
    // All events use the same context object.
    let context = {
      tenant: self,
      request: request,
      response: response
    };
    self.emit(Tenant.startRequestEvent, context);
    self.emit(Tenant.handleRequestEvent, context);
    self.emit(Tenant.endRequestEvent, context);
    if (request.tearDown) request.tearDown();
    this.log('info', 'Handled request', {
      tenant: self.name,
      url: request.url,
      status: response.statusCode
    });
    if (next) next();
  };
}

/**
 * An empty shell that you can use to initialize services if you don't need multitenancy.
 */
Tenant.empty = new Tenant({
  name: "Empty shell"
});

/**
 * The list of tenants.
 */
Tenant.list = {};

/**
 * The names of tenants.
 */
 Tenant.names = [];

function localHandler(server) {
  return function defaultHandler(req, res) {
    let d = domain.create();
    d.on('error', function onError(err) {
      console.error('Unrecoverable error', err.stack || err.message || err);
      try {
        // Close down within 30 seconds
        let killTimer = setTimeout(function processKill() {
          process.exit(1);
        }, 30000);
        // But don't keep the process open just for that!
        killTimer.unref();

        // stop taking new requests.
        server.close();

        if (res && !res.finished) {
          // try to send an error to the request that triggered the problem
          res.statusCode = 500;
          // TODO: let route handlers set headers, including powered by.
          res.end('Oops, the server choked on this request!\n');
          // TODO: broadcast the error to give loggers a chance to use it.
        }
      } catch (er2) {
        // oh well, not much we can do at this point.
        console.error('Error sending 500!', er2.stack);
        return;
      }
    });
    d.add(req);
    d.add(res);
    let tenant = Tenant.resolve(req);
    if (!tenant) {
      console.error('Could not resolve shell.', {url: req.url, host: req.headers.host});
      return;
    }
    d.add(tenant);

    // Now run the handler function in the domain.
    d.run(function runHandler() {
      tenant.handleRequest(req, res, function() {
        res.end('');
      });
    });
  }
}

/**
 * @description
 * The event that is broadcast at the beginning of request handling.
 * This is a good time to attach a service to the request object.
 * @type {string}
 */
Tenant.startRequestEvent = 'decent.multi-tenancy.start-request';
Tenant.startRequestEvent_payload = {
  tenant: 'Tenant',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * The event that is broadcast when the request is ready to be handled.
 * @type {string}
 */
Tenant.handleRequestEvent = 'decent.multi-tenancy.handle-request';
Tenant.handleRequestEvent_payload = {
  tenant: 'Tenant',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * The event that is broadcast at the end of request handling.
 * This is a good time to clean-up, detach services and events.
 * @type {string}
 */
Tenant.endRequestEvent = 'decent.multi-tenancy.end-request';
Tenant.endRequestEvent_payload = {
  tenant: 'Tenant',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

module.exports = Tenant;
<commit_msg>Remove domain-based error treatment following domain deprecation. Make local handler a Lambda.<commit_after>// Decent Multi-Tenancy (c) 2014-2015 Bertrand Le Roy, under MIT. See LICENSE.txt for licensing details.
'use strict';

// TODO: Add configurable timeout to request handling.
// TODO: Enable tenants to be restarted.
// TODO: add opt-out for debug mode.
// TODO: emit errors up onto the tenant.

let EventEmitter = require('events').EventEmitter;
let util = require('util');
let path = require('path');
let fs = require('fs');
let http = require('http');
let https = require('https');

/**
 * A tenant is an independant site in the system.
 */
class Tenant extends EventEmitter {
  /**
  * One should usually refrain from using this constructor directly, and do a `Tenant.load` instead.
  * @param {Object} options
  * @param {String} [options.name] The technical name of the tenant. Default is "default".
  * @param {String} [options.displayName] The display name of the tenant. Default is `name`.
  * @param {String} [options.host] The host name under which the tenant answers. If not specified, this is process.env.IP, or `localhost`.
  * @param {String} [options.debugHost] A host name under which the tenant answers, that causes `request.debug` to be true.
  * @param {Number} [options.port] The port to which the tenant answers. Defaults to `process.env.port`, or 80 if that's not found.
  * @param {Number} [options.debugPort] The port associated with the debug host. Defaults to `process.env.port`, or 1337 if that's not found.
  * @param {Boolean} [options.https] True if the site must use HTTPS. False by default.
  * @param {Boolean} [options.debugHttps] True if the site must use HTTPS in debug mode. False by default.
  * @param {String} [options.cert] The path to the SSL certificate to use with this tenant.
  * @param {String} [options.key] The path to the SSL key to use with this tenant.
  * @param {String} [options.pfx] The path to the pfx SSL certificate to use with this tenant.
  * @param {String} [options.debugCert] The path to the SSL certificate to use with this tenant in debug mode. Defaults to `cert`.
  * @param {String} [options.debugKey] The path to the SSL key to use with this tenant in debug mode. Defaults to `key`.
  * @param {String} [options.debugPfx] The path to the pfx SSL certificate to use with this tenant in debug mode. Defaults to `pfx`.
  * @param {Boolean} [options.active] True if the tenant is active.
  * @param {Object} [options.logger] A logger object with a log(level:string, message:string) method. If not specified, log messages go to the console.
  */
  constructor(options) {
    super();
    Object.assign(this, {
      settings: options || {},
      name: options.name || 'default',
      displayName: options.displayName || options.name,
      host: Array.isArray(options.host)
        ? options.host
        : (options.host
          ? [options.host]
          : (process.env.IP
            ? [process.env.IP]
            : ['localhost'])),
      debugHost: Array.isArray(options.debugHost)
        ? options.debugHost
        : (options.debugHost ? [options.debugHost] : ['localhost']),
      port: options.port || process.env.PORT || 80,
      debugPort: options.debugPort || process.env.PORT || 1337,
      https: !!options.https,
      debugHttps: !!options.debugHttps,
      cert: options.cert,
      key: options.key,
      pfx: options.pfx,
      debugCert: options.debugCert || options.cert,
      debugKey: options.debugKey || options.key,
      debugPfx: options.debugPfx || options.pfx,
      features: options.features || {},
      active: !(options.active === false),
      log: (options.logger ? options.logger.log : null) ||
        function log() {
          console.log.apply(null, arguments);
        }
    });
  }

  /**
   * Creates a tenant from a settings file or object, and adds it to `Tenant.list` and `Tenant.names`.
   * @param {String|Object} settingsh The path of the settings file, or a settings object.
   * @param {Object} [defaults] Default settings.
   * @returns {Tenant} The new tenant.
   */
  static load(settings, defaults) {
    defaults = defaults || {};
    if (typeof settings == 'string') {
      settings = require(settings);
    }
    for (let settingName of Object.getOwnPropertyNames(defaults)) {
      if (!settings.hasOwnProperty(settingName)) {
        settings[settingName] = defaults[settingName];
      }
    }
    let tenant = new Tenant(settings);
    Tenant.list[tenant.name] = tenant;
    Tenant.names.push(tenant.name);
    return tenant;
  };

  /**
   * Discovers all tenants in the rootPath directory.
   * This method scans the directory for subdirectories with `settings.json` files
   * in them. Those files are then loaded as site settings.
   * @param {Object} defaults The default settings for the shells.
   * @param {String} rootPath The root path where to look for shell settings files. Defaults to "./sites".
   */
  static discover(defaults, rootPath) {
    rootPath = rootPath || './sites';
    let siteNames = fs.readdirSync(rootPath);
    for (let siteName of siteNames) {
      if (siteName[0] === '.') return;
      let resolvedSitePath = path.resolve(rootPath, siteName, 'settings.json');
      let settings;
      try {
        settings = require(resolvedSitePath);
      }
      catch(ex) {
        ex.path = resolvedSitePath;
        ex.message = 'Failed to load site settings for ' + siteName;
        throw ex;
      }
      settings.settingsPath = resolvedSitePath;
      settings.rootPath = resolvedSitePath;
      settings.name = settings.name || siteName;
      Tenant.load(settings, defaults);
    }
  };

  /**
   * Returns the tenant that should handle this request.
   * @param {IncomingMessage} request The request.
   * @returns {Tenant} The tenant that should handle the request, null if no active tenant is fit to do it.
   */
  static resolve(request) {
    // If there's only one shell, always return that.
    if (Tenant.names.length === 1) return Tenant.list[Tenant.names[0]];
    // Otherwise let each shell decide if it can handle the request.
    for (let shellName of Tenant.names) {
      let shell = Tenant.list[shellName];

      if (shell.active && shell.canHandle(request)) {
        return shell;
      }
    }
    // Unresolved requests should not go to a default shell
    // if there's more than one.
    return null;
  };

  /**
   * Starts http listeners covering for all active tenants in `Tenant.list`.
    let httpServer;
   */
  static listen() {
    let httpServer;
    let port = process.env.PORT || 1337;

    let handler = (req, res) => {
      let tenant = Tenant.resolve(req);
      if (!tenant) {
        console.error('Could not resolve shell.', {url: req.url, host: req.headers.host});
        return;
      }
      tenant.handleRequest(req, res, function(err) {
        if (err) {
          throw err;
        }
        res.end('');
      });
    }
    // If iisnode, only create one server.
    // TODO: properly handle https on IIS.
    if (process.env.IISNODE_VERSION && port.substr(0, 9) === '\\\\.\\pipe\\') {
      httpServer = new http.Server();
      httpServer.on('request', handler);
      httpServer.listen(port);
      return;
    }
    // Listen for each tenant
    for (let shellName of Tenant.names) {
      let tenant = Tenant.list[shellName];
      if (!tenant.active) continue;
      let hosts = Array.isArray(tenant.host) ? tenant.host : [tenant.host];
      for (let host of hosts) {
        let currentPort = tenant.port !== '*' ? tenant.port : port;
        // TODO: create debug servers as needed.
        httpServer = tenant.https
            ? https.createServer({
              host: host,
              key: tenant.key,
              cert: tenant.cert,
              pfx: tenant.pfx
            })
            : new http.Server();
        httpServer.on('request', handler);
        httpServer.listen(currentPort);
        tenant.log('info', `${tenant.name} started listening on port ${currentPort}.`)
      }
    }
  }

  /**
   * Determines if the tenant can handle that request.
   * @param request The request
   * @returns {Boolean} True if the tenant can handle the request.
   */
  canHandle(request) {
    let host = request.headers.host;
    let hosts = this.host.concat(this.debugHost);
    for (let i = 0; i < hosts.length; i++) {
      let thisHost = hosts[i];
      if ((
        (
        ((this.https && this.port === 443) || this.port === 80 || this.port === '*')
        && thisHost === host
        )
        || (this.port === '*' && host.startsWith(thisHost))
        || (thisHost + ':' + this.port === host)
      ) && (
      !this.path
      || request.url.startsWith(this.path)
      )) {
        if (i >= this.host.length) {
          request.debug = true;
          // We also set the tenant into debug mode, which could cause problems
          // if the same tenant is on a server configured and able to answer on both
          // the host and debug host. That should however never happen.
          this.debug = request.debug;
        }
        return true;
      }
    }
    return false;
  };

  /**
   * Enables or disables the tenant.
   * @param {Boolean} state If provided, sets the enabled state of the tenant. Otherwise, enables the tenant.
   */
  enable(state) {
    this.active = typeof state === 'undefined' || !!state;
  };

  /**
   * Middleware for use, for example, with Express.
   * @param {http.IncomingMessage} request Request
   * @param {http.ServerResponse} response Response
   * @param {Function} next The callback.
   */
  middleware(request, response, next) {
    if (!this.canHandle(request)) {
      next();
      return;
    }
    this.handleRequest(request, response, next);
  };

  /**
   * Handles the request for the tenant.
   * Emits the following events:
   * * decent.multi-tenancy.start-request {tenant, request, response}
   * * decent.multi-tenancy.handle-request {tenant, request, response}
   * * decent.multi-tenancy.end-request {tenant, request, response}
   * * error
   * @param request Request
   * @param response Response
   * @param {Function} next The callback
   */
  handleRequest(request, response, next) {
    let self = this;
    let log = this.log;
    // All events use the same context object.
    let context = {
      tenant: self,
      request: request,
      response: response
    };
    self.emit(Tenant.startRequestEvent, context);
    self.emit(Tenant.handleRequestEvent, context);
    self.emit(Tenant.endRequestEvent, context);
    if (request.tearDown) request.tearDown();
    this.log('info', 'Handled request', {
      tenant: self.name,
      url: request.url,
      status: response.statusCode
    });
    if (next) next();
  };
}

/**
 * An empty shell that you can use to initialize services if you don't need multitenancy.
 */
Tenant.empty = new Tenant({
  name: "Empty shell"
});

/**
 * The list of tenants.
 */
Tenant.list = {};

/**
 * The names of tenants.
 */
Tenant.names = [];

/**
 * @description
 * The event that is broadcast at the beginning of request handling.
 * This is a good time to attach a service to the request object.
 * @type {string}
 */
Tenant.startRequestEvent = 'decent.multi-tenancy.start-request';
Tenant.startRequestEvent_payload = {
  tenant: 'Tenant',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * The event that is broadcast when the request is ready to be handled.
 * @type {string}
 */
Tenant.handleRequestEvent = 'decent.multi-tenancy.handle-request';
Tenant.handleRequestEvent_payload = {
  tenant: 'Tenant',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * The event that is broadcast at the end of request handling.
 * This is a good time to clean-up, detach services and events.
 * @type {string}
 */
Tenant.endRequestEvent = 'decent.multi-tenancy.end-request';
Tenant.endRequestEvent_payload = {
  tenant: 'Tenant',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

module.exports = Tenant;
<|endoftext|>