<commit_before>var http = require('http');
var path = require('path');
var url = require('url');
var querystring = require('querystring');
var async = require('async');
var spdy = require('spdy');
var argo = require('argo');
var titan = require('titan');
var WebSocketServer = require('ws').Server;
var SpdyAgent = require('./spdy_agent');
var EventBroker = require('./event_broker');
var PeerSocket = require('./peer_socket');
var EventSocket = require('./event_socket');
var Siren = require('argo-formatter-siren');
var DevicesResource = require('./api_resources/devices');
var PeersManagementResource = require('./api_resources/peer_management');
var RootResource = require('./api_resources/root');
var ServersResource = require('./api_resources/servers');
var deviceFormatter = require('./api_formats/siren/device.siren');
var rels = require('zetta-rels');

var querytopic = require('./query_topic');

var ZettaHttpServer = module.exports = function(zettaInstance) {
  var self = this;
  this.idCounter = 0;
  this.zetta = zettaInstance;
  this.peerRegistry = zettaInstance.peerRegistry;
  this.eventBroker = new EventBroker(zettaInstance);
  this.clients = {};
  this.peers = {}; // connected peers

  this._deviceQueries = [];

  this._collectors = {};
  this.server = spdy.createServer({
    windowSize: 1024 * 1024,
    plain: true,
    ssl: false
  });
  this.spdyServer = spdy.createServer({
    windowSize: 1024 * 1024,
    plain: true,
    ssl: false
  });

  var ValidWSUrls = [
      /^\/events$/, // /events
      /^\/events\?.+$/, // /events?topic=query:where type="led"
      /^\/servers\/(.+)\/events/, // /servers/BC2832FD-9437-4473-A4A8-AC1D56B12C61/events
      /^\/peers\/(.+)$/, // /peers/123123...
      /^\/peer-management$/, // /peer-management
  ];

  function match(request) {
    return ValidWSUrls.some(function(re) {
      return re.test(request.url);
    });
  }

  this.wss = new WebSocketServer({ noServer: true });
  this.server.on('upgrade', function(request, socket, headers) {
    if(match(request)) {
      self.wss.handleUpgrade(request, socket, headers, function(ws) {
        var match = /^\/peers\/(.+)$/.exec(ws.upgradeReq.url);
        if (match) {
          var name = /^\/peers\/(.+)$/.exec(url.parse(ws.upgradeReq.url, true).pathname)[1];
          name = decodeURI(name);
          self.zetta.log.emit('log', 'http_server', 'Websocket connection for peer "' + name + '" established.');

          if (self.peers[name] && self.peers[name].state !== PeerSocket.DISCONNECTED) {
            // peer already connected or connecting
            ws.close(4000, 'peer already connected');
          } else if (self.peers[name]) {
            // peer has been disconnected but has connected before.
            self.peers[name].init(ws);
          } else {
            var peer = new PeerSocket(ws, name, self.peerRegistry);
            self.peers[name] = peer;

            // Events coming from the peers pubsub using push streams
            peer.on('zetta-events', function(topic, data) {
              self.zetta.pubsub.publish(name + '/' + topic, data, true); // Set fromRemote flag to true
            });

            peer.on('connected', function() {
              self.eventBroker.peer(peer);
              self.zetta.log.emit('log', 'http_server', 'Peer connection established "' + name + '".');
              self.zetta.pubsub.publish('_peer/connect', { peer: peer });
            });

            peer.on('error', function(err) {
              self.zetta.log.emit('log', 'http_server', 'Peer connection failed for "' + name + '".');
              self.zetta.pubsub.publish('_peer/disconnect', { peer: peer });
            });

            peer.on('end', function() {
              self.zetta.log.emit('log', 'http_server', 'Peer connection closed for "' + name + '".');
              self.zetta.pubsub.publish('_peer/disconnect', { peer: peer });
            });
          }
        } else if (ws.upgradeReq.url === '/peer-management') {
          var query = [
            { name: self.zetta.id, topic: '_peer/connect' },
            { name: self.zetta.id, topic: '_peer/disconnect' }];

          var client = new EventSocket(ws, query);
          self.eventBroker.client(client);
        } else {
          self.setupEventSocket(ws);
        }
      });
    } else {
      // Check any custom websocket paths from extentions
      var endWith404 = function() {
        var responseLine = 'HTTP/1.1 404 Server Not Found\r\n\r\n\r\n';
        socket.end(responseLine);
      };

      if (self.server.listeners('upgrade').length > 1) {
        var timer = setTimeout(function() {
          if (socket.bytesWritten === 0) {
            endWith404();
          }
        }, 5000);
        socket.on('close', function() {
          clearTimeout(timer);
        });
      } else {
        endWith404();
      }
    }
  });

  this.cloud = argo()
    .use(titan)
   .format({ engines: [Siren], override: { 'application/json': Siren }, directory: path.join(__dirname, './api_formats') })
    .add(RootResource, zettaInstance)
    .add(PeersManagementResource, zettaInstance)
    .add(DevicesResource, zettaInstance)
    .add(ServersResource, zettaInstance)
    .allow({
      methods: ['DELETE', 'PUT', 'PATCH', 'POST'],
      origins: ['*'],
      headers: ['accept', 'content-type'],
      maxAge: '432000'
    })
    .use(function(handle) {
      handle('request', function(env, next) {
        if (env.request.method === 'OPTIONS') {
          env.argo._routed = true;
        }
        next(env);
      });
    })
    .use(function(handle) {
      handle('request', function(env, next) {
        // stop execution in argo for initiate peer requests, handled by peer_client
        if (!(/^\/_initiate_peer\/(.+)$/.exec(env.request.url)) ) {
          next(env);
        }
      });
    });
};

ZettaHttpServer.prototype.init = function(cb) {
  var self = this;

  // handle http registration of device
  this.cloud = this.cloud.use(this.httpRegistration.bind(this));
  // handle proxying to peer
  //this.cloud = this.cloud.route('*', this.proxyToPeer.bind(this));
  // setup http servers request handler to argo routes
  this.cloud = this.cloud.build();

  this.server.on('request', this.cloud.run);
  this.spdyServer.on('request', this.cloud.run);

  if (cb) {
    cb();
  }
};

ZettaHttpServer.prototype.listen = function() {
  this.server.listen.apply(this.server,arguments);
  return this;
};

ZettaHttpServer.prototype.collector = function(name, collector) {
  if(typeof name === 'function'){
    collector = name;
    name = '_logs';
  }

  if(!this._collectors[name]) {
    this._collectors[name] = [];
  }

  this._collectors[name].push(collector);
  return this;
};

function getCurrentProtocol(req) {
  var xfp = req.headers['x-forwarded-proto'];
  var protocol;

  if (xfp && xfp.length) {
    protocol = xfp.replace(/\s*/, '').split(',')[0];
  } else {
    protocol = req.connection.encrypted ? 'https' : 'http';
  }

  return protocol;
}

ZettaHttpServer.prototype.wireUpWebSocketForEvent = function(ws, host, p) {
  ws._env = { helpers: {}};
  ws._loader = { path: p };

  ws._env.uri = function() {
    var protocol = getCurrentProtocol(ws.upgradeReq);

    if (!host) {
      var address = ws.upgradeReq.connection.address();
      host = address.address;
      if (address.port) {
        if (!(protocol === 'https' && address.port === 443) &&
            !(protocol === 'http' && address.port === 80)) {
          host += ':' + address.port
        }
      }
    }

    return protocol + '://' + path.join(host, ws.upgradeReq.url);
  };

  ws._env.helpers.url = {};
  ws._env.helpers.url.path = function(pathname) {
    var parsed = url.parse(ws._env.uri());
    parsed.search = null;
    parsed.pathname = pathname;
    return url.format(parsed);
  };
};

ZettaHttpServer.prototype.setupEventSocket = function(ws) {
  var self = this;
  var host = ws.upgradeReq.headers['host'];

  if (/^\/events/.exec(ws.upgradeReq.url)) {
    self.wireUpWebSocketForEvent(ws, host, '/servers/' + self.zetta._name);
    var queryString = url.parse(ws.upgradeReq.url).query;

    if(!queryString) {
      var client = new EventSocket(ws, null, true);
      self.eventBroker.client(client);
      return;
    }

    var query = querystring.parse(queryString);

    function copy(q) {
      var c = {};
      Object.keys(q).forEach(function(k) {
        c[k] = q[k];
      });

      return c;
    }

    [self.zetta._name].concat(Object.keys(self.peers)).forEach(function(serverId) {
      var q = copy(query);
      q.name = serverId;

      if (q.topic) {
        var qt = querytopic.parse(query.topic);
        if (qt) {
          q.topic = querytopic.format(qt);
        }
        var client = new EventSocket(ws, q, false);
        self.eventBroker.client(client);
      }
    });

    function subscribeOnPeerConnect(e, obj) {
      var q = copy(query);
      q.name = obj.peer.name;

      if (q.topic) {
        var qt = querytopic.parse(query.topic);
        if (qt) {
          q.topic = querytopic.format(qt);
        }

        var client = new EventSocket(ws, q);
        self.eventBroker.client(client);
      }
    }

    ws.on('close', function() {
      self.zetta.pubsub.unsubscribe('_peer/connect', subscribeOnPeerConnect);
    });

    self.zetta.pubsub.subscribe('_peer/connect', subscribeOnPeerConnect);
  } else {
    var match = /^\/servers\/(.+)\/events/.exec(ws.upgradeReq.url);
    if(!match) {
      ws.close(1001); // go away status code
      return;
    }

    var query = querystring.parse(url.parse(ws.upgradeReq.url).query);
    query.serverId = match[1]; // set serverId on query

    self.wireUpWebSocketForEvent(ws, host, '/servers/' + query.serverId);

    var query = querystring.parse(url.parse(ws.upgradeReq.url).query);
    query.name = decodeURI(match[1]);

    if (query.topic) {
      var qt = querytopic.parse(query.topic);
      if (qt) {
        query.topic = querytopic.format(qt);
      }
      var client = new EventSocket(ws, query);
      self.eventBroker.client(client);
    }
  }
};

ZettaHttpServer.prototype.httpRegistration = function(handle) {
  handle('request', function(env, next) {
    if (!(env.request.method === 'POST' && env.request.url === '/registration')) {
      return next(env);
    }

    env.request.getBody(function(err, body) {
      body = JSON.parse(body.toString());
      var peer = self.peers[body.target];

      if (!peer.agent) {
        env.response.statusCode = 404;
        return next(env);
      }

      env.request.body = new Buffer(JSON.stringify(body.device));
      env.zettaAgent = peer.agent;
      next(env);
    });
  });
};

ZettaHttpServer.prototype.proxyToPeers = function(peers, env, cb) {
  var self = this;

  var req = env.request;
  var res = env.response;
  var protocol = getCurrentProtocol(req);

  var messageId = ++self.idCounter;
  self.clients[messageId] = res;

  var tasks = peers.map(function(p) {
    var name = p.id;
    return function(callback) {

      var reqUrl = req.templateUrl.replace('{{peerName}}', encodeURIComponent(name));
      var headers = {};

      Object.keys(req.headers).forEach(function(key) {
        headers[key] = req.headers[key];
      });

      if (!req.isSpdy) {
        headers['x-forwarded-proto'] = protocol;
      }

      var peer = self.peers[name];
      if (!peer || peer.state !== PeerSocket.CONNECTED){
        callback(null, { err: new Error('Peer does not exist.') });
        return;
      }

      var agent = env.zettaAgent || peer.agent;

      var opts = { method: req.method, headers: headers, path: reqUrl, agent: agent };
      var request = http.request(opts, function(response) {
        response.getBody(function(err, body) {
          callback(null, { res: response, err: err, body: body });
        });
      }).on('error', function(err) {
        return callback(null, { err: err });
      });

      if (req.body) {
        request.end(req.body);
      } else {
        req.pipe(request);
      }
    };
  });

  async.parallelLimit(tasks, 5, function(err, results) {
    cb(err, results, messageId);
  });
};

ZettaHttpServer.prototype.proxyToPeer = function(env, next) {
  var self = this;
  var req = env.request;
  var res = env.response;
  var opts = env.proxyOpts || {};

  var messageId = ++self.idCounter;

  // change this to handle multiple fogs
  self.clients[messageId] = res;//req.socket; Will need socket for event broadcast.

  var parsed = url.parse(req.url);
  var name = decodeURIComponent(parsed.pathname.split('/')[2]);

  if (!req.isSpdy) {
    req.headers['x-forwarded-proto'] = getCurrentProtocol(req);
  }

  var peer = self.peers[name];
  if (!peer || peer.state !== PeerSocket.CONNECTED){
    res.statusCode = 404;
    res.end();
    return;
  }

  var agent = env.zettaAgent || peer.agent;

  var opts = { method: req.method, headers: req.headers, path: req.url, agent: agent };
  var request = http.request(opts, function(response) {
    var res = self.clients[messageId];

    if (!res) {
      response.statusCode = 404;
      return;
    }

    Object.keys(response.headers).forEach(function(header) {
      res.setHeader(header, response.headers[header]);
    });

    res.statusCode = response.statusCode;

    if(!opts.pipe) {
      var body = null;
      var buf = [];
      var len = 0;

      response.on('readable', function() {
        var chunk;

        while ((chunk = response.read()) != null) {
          buf.push(chunk);
          len += chunk.length;
        }

        if (!buf.length) {
          return;
        }

        body = new Buffer(len);
        var i = 0;
        buf.forEach(function(chunk) {
          chunk.copy(body, i, 0, chunk.length);
          i += chunk.length;
        });
      });
    } else {
      response.pipe(res);
    }

    response.on('end', function() {
      if(!opts.pipe) {
        env.response.body = body;
      }
      delete self.clients[messageId];
      next(env);
    });

  }).on('error', function(err) {
    env.response.statusCode = 502;
    return next(env);
  });

  if (req.body) {
    request.end(req.body);
  } else {
    req.pipe(request);
  }
};
<commit_msg>When proxying req to hub use piping unless told not to.<commit_after>var http = require('http');
var path = require('path');
var url = require('url');
var querystring = require('querystring');
var async = require('async');
var spdy = require('spdy');
var argo = require('argo');
var titan = require('titan');
var WebSocketServer = require('ws').Server;
var SpdyAgent = require('./spdy_agent');
var EventBroker = require('./event_broker');
var PeerSocket = require('./peer_socket');
var EventSocket = require('./event_socket');
var Siren = require('argo-formatter-siren');
var DevicesResource = require('./api_resources/devices');
var PeersManagementResource = require('./api_resources/peer_management');
var RootResource = require('./api_resources/root');
var ServersResource = require('./api_resources/servers');
var deviceFormatter = require('./api_formats/siren/device.siren');
var rels = require('zetta-rels');

var querytopic = require('./query_topic');

var ZettaHttpServer = module.exports = function(zettaInstance) {
  var self = this;
  this.idCounter = 0;
  this.zetta = zettaInstance;
  this.peerRegistry = zettaInstance.peerRegistry;
  this.eventBroker = new EventBroker(zettaInstance);
  this.clients = {};
  this.peers = {}; // connected peers

  this._deviceQueries = [];

  this._collectors = {};
  this.server = spdy.createServer({
    windowSize: 1024 * 1024,
    plain: true,
    ssl: false
  });
  this.spdyServer = spdy.createServer({
    windowSize: 1024 * 1024,
    plain: true,
    ssl: false
  });

  var ValidWSUrls = [
      /^\/events$/, // /events
      /^\/events\?.+$/, // /events?topic=query:where type="led"
      /^\/servers\/(.+)\/events/, // /servers/BC2832FD-9437-4473-A4A8-AC1D56B12C61/events
      /^\/peers\/(.+)$/, // /peers/123123...
      /^\/peer-management$/, // /peer-management
  ];

  function match(request) {
    return ValidWSUrls.some(function(re) {
      return re.test(request.url);
    });
  }

  this.wss = new WebSocketServer({ noServer: true });
  this.server.on('upgrade', function(request, socket, headers) {
    if(match(request)) {
      self.wss.handleUpgrade(request, socket, headers, function(ws) {
        var match = /^\/peers\/(.+)$/.exec(ws.upgradeReq.url);
        if (match) {
          var name = /^\/peers\/(.+)$/.exec(url.parse(ws.upgradeReq.url, true).pathname)[1];
          name = decodeURI(name);
          self.zetta.log.emit('log', 'http_server', 'Websocket connection for peer "' + name + '" established.');

          if (self.peers[name] && self.peers[name].state !== PeerSocket.DISCONNECTED) {
            // peer already connected or connecting
            ws.close(4000, 'peer already connected');
          } else if (self.peers[name]) {
            // peer has been disconnected but has connected before.
            self.peers[name].init(ws);
          } else {
            var peer = new PeerSocket(ws, name, self.peerRegistry);
            self.peers[name] = peer;

            // Events coming from the peers pubsub using push streams
            peer.on('zetta-events', function(topic, data) {
              self.zetta.pubsub.publish(name + '/' + topic, data, true); // Set fromRemote flag to true
            });

            peer.on('connected', function() {
              self.eventBroker.peer(peer);
              self.zetta.log.emit('log', 'http_server', 'Peer connection established "' + name + '".');
              self.zetta.pubsub.publish('_peer/connect', { peer: peer });
            });

            peer.on('error', function(err) {
              self.zetta.log.emit('log', 'http_server', 'Peer connection failed for "' + name + '".');
              self.zetta.pubsub.publish('_peer/disconnect', { peer: peer });
            });

            peer.on('end', function() {
              self.zetta.log.emit('log', 'http_server', 'Peer connection closed for "' + name + '".');
              self.zetta.pubsub.publish('_peer/disconnect', { peer: peer });
            });
          }
        } else if (ws.upgradeReq.url === '/peer-management') {
          var query = [
            { name: self.zetta.id, topic: '_peer/connect' },
            { name: self.zetta.id, topic: '_peer/disconnect' }];

          var client = new EventSocket(ws, query);
          self.eventBroker.client(client);
        } else {
          self.setupEventSocket(ws);
        }
      });
    } else {
      // Check any custom websocket paths from extentions
      var endWith404 = function() {
        var responseLine = 'HTTP/1.1 404 Server Not Found\r\n\r\n\r\n';
        socket.end(responseLine);
      };

      if (self.server.listeners('upgrade').length > 1) {
        var timer = setTimeout(function() {
          if (socket.bytesWritten === 0) {
            endWith404();
          }
        }, 5000);
        socket.on('close', function() {
          clearTimeout(timer);
        });
      } else {
        endWith404();
      }
    }
  });

  this.cloud = argo()
    .use(titan)
   .format({ engines: [Siren], override: { 'application/json': Siren }, directory: path.join(__dirname, './api_formats') })
    .add(RootResource, zettaInstance)
    .add(PeersManagementResource, zettaInstance)
    .add(DevicesResource, zettaInstance)
    .add(ServersResource, zettaInstance)
    .allow({
      methods: ['DELETE', 'PUT', 'PATCH', 'POST'],
      origins: ['*'],
      headers: ['accept', 'content-type'],
      maxAge: '432000'
    })
    .use(function(handle) {
      handle('request', function(env, next) {
        if (env.request.method === 'OPTIONS') {
          env.argo._routed = true;
        }
        next(env);
      });
    })
    .use(function(handle) {
      handle('request', function(env, next) {
        // stop execution in argo for initiate peer requests, handled by peer_client
        if (!(/^\/_initiate_peer\/(.+)$/.exec(env.request.url)) ) {
          next(env);
        }
      });
    });
};

ZettaHttpServer.prototype.init = function(cb) {
  var self = this;

  // handle http registration of device
  this.cloud = this.cloud.use(this.httpRegistration.bind(this));
  // handle proxying to peer
  //this.cloud = this.cloud.route('*', this.proxyToPeer.bind(this));
  // setup http servers request handler to argo routes
  this.cloud = this.cloud.build();

  this.server.on('request', this.cloud.run);
  this.spdyServer.on('request', this.cloud.run);

  if (cb) {
    cb();
  }
};

ZettaHttpServer.prototype.listen = function() {
  this.server.listen.apply(this.server,arguments);
  return this;
};

ZettaHttpServer.prototype.collector = function(name, collector) {
  if(typeof name === 'function'){
    collector = name;
    name = '_logs';
  }

  if(!this._collectors[name]) {
    this._collectors[name] = [];
  }

  this._collectors[name].push(collector);
  return this;
};

function getCurrentProtocol(req) {
  var xfp = req.headers['x-forwarded-proto'];
  var protocol;

  if (xfp && xfp.length) {
    protocol = xfp.replace(/\s*/, '').split(',')[0];
  } else {
    protocol = req.connection.encrypted ? 'https' : 'http';
  }

  return protocol;
}

ZettaHttpServer.prototype.wireUpWebSocketForEvent = function(ws, host, p) {
  ws._env = { helpers: {}};
  ws._loader = { path: p };

  ws._env.uri = function() {
    var protocol = getCurrentProtocol(ws.upgradeReq);

    if (!host) {
      var address = ws.upgradeReq.connection.address();
      host = address.address;
      if (address.port) {
        if (!(protocol === 'https' && address.port === 443) &&
            !(protocol === 'http' && address.port === 80)) {
          host += ':' + address.port
        }
      }
    }

    return protocol + '://' + path.join(host, ws.upgradeReq.url);
  };

  ws._env.helpers.url = {};
  ws._env.helpers.url.path = function(pathname) {
    var parsed = url.parse(ws._env.uri());
    parsed.search = null;
    parsed.pathname = pathname;
    return url.format(parsed);
  };
};

ZettaHttpServer.prototype.setupEventSocket = function(ws) {
  var self = this;
  var host = ws.upgradeReq.headers['host'];

  if (/^\/events/.exec(ws.upgradeReq.url)) {
    self.wireUpWebSocketForEvent(ws, host, '/servers/' + self.zetta._name);
    var queryString = url.parse(ws.upgradeReq.url).query;

    if(!queryString) {
      var client = new EventSocket(ws, null, true);
      self.eventBroker.client(client);
      return;
    }

    var query = querystring.parse(queryString);

    function copy(q) {
      var c = {};
      Object.keys(q).forEach(function(k) {
        c[k] = q[k];
      });

      return c;
    }

    [self.zetta._name].concat(Object.keys(self.peers)).forEach(function(serverId) {
      var q = copy(query);
      q.name = serverId;

      if (q.topic) {
        var qt = querytopic.parse(query.topic);
        if (qt) {
          q.topic = querytopic.format(qt);
        }
        var client = new EventSocket(ws, q, false);
        self.eventBroker.client(client);
      }
    });

    function subscribeOnPeerConnect(e, obj) {
      var q = copy(query);
      q.name = obj.peer.name;

      if (q.topic) {
        var qt = querytopic.parse(query.topic);
        if (qt) {
          q.topic = querytopic.format(qt);
        }

        var client = new EventSocket(ws, q);
        self.eventBroker.client(client);
      }
    }

    ws.on('close', function() {
      self.zetta.pubsub.unsubscribe('_peer/connect', subscribeOnPeerConnect);
    });

    self.zetta.pubsub.subscribe('_peer/connect', subscribeOnPeerConnect);
  } else {
    var match = /^\/servers\/(.+)\/events/.exec(ws.upgradeReq.url);
    if(!match) {
      ws.close(1001); // go away status code
      return;
    }

    var query = querystring.parse(url.parse(ws.upgradeReq.url).query);
    query.serverId = match[1]; // set serverId on query

    self.wireUpWebSocketForEvent(ws, host, '/servers/' + query.serverId);

    var query = querystring.parse(url.parse(ws.upgradeReq.url).query);
    query.name = decodeURI(match[1]);

    if (query.topic) {
      var qt = querytopic.parse(query.topic);
      if (qt) {
        query.topic = querytopic.format(qt);
      }
      var client = new EventSocket(ws, query);
      self.eventBroker.client(client);
    }
  }
};

ZettaHttpServer.prototype.httpRegistration = function(handle) {
  handle('request', function(env, next) {
    if (!(env.request.method === 'POST' && env.request.url === '/registration')) {
      return next(env);
    }

    env.request.getBody(function(err, body) {
      body = JSON.parse(body.toString());
      var peer = self.peers[body.target];

      if (!peer.agent) {
        env.response.statusCode = 404;
        return next(env);
      }

      env.request.body = new Buffer(JSON.stringify(body.device));
      env.zettaAgent = peer.agent;
      next(env);
    });
  });
};

ZettaHttpServer.prototype.proxyToPeers = function(peers, env, cb) {
  var self = this;

  var req = env.request;
  var res = env.response;
  var protocol = getCurrentProtocol(req);

  var messageId = ++self.idCounter;
  self.clients[messageId] = res;

  var tasks = peers.map(function(p) {
    var name = p.id;
    return function(callback) {

      var reqUrl = req.templateUrl.replace('{{peerName}}', encodeURIComponent(name));
      var headers = {};

      Object.keys(req.headers).forEach(function(key) {
        headers[key] = req.headers[key];
      });

      if (!req.isSpdy) {
        headers['x-forwarded-proto'] = protocol;
      }

      var peer = self.peers[name];
      if (!peer || peer.state !== PeerSocket.CONNECTED){
        callback(null, { err: new Error('Peer does not exist.') });
        return;
      }

      var agent = env.zettaAgent || peer.agent;

      var opts = { method: req.method, headers: headers, path: reqUrl, agent: agent };
      var request = http.request(opts, function(response) {
        response.getBody(function(err, body) {
          callback(null, { res: response, err: err, body: body });
        });
      }).on('error', function(err) {
        return callback(null, { err: err });
      });

      if (req.body) {
        request.end(req.body);
      } else {
        req.pipe(request);
      }
    };
  });

  async.parallelLimit(tasks, 5, function(err, results) {
    cb(err, results, messageId);
  });
};

ZettaHttpServer.prototype.proxyToPeer = function(env, next) {
  var self = this;
  var req = env.request;
  var res = env.response;

  var parsed = url.parse(req.url);
  var name = decodeURIComponent(parsed.pathname.split('/')[2]);

  if (!req.isSpdy) {
    req.headers['x-forwarded-proto'] = getCurrentProtocol(req);
  }

  var peer = self.peers[name];
  if (!peer || peer.state !== PeerSocket.CONNECTED){
    res.statusCode = 404;
    res.end();
    return;
  }

  var agent = env.zettaAgent || peer.agent;

  var opts = {
    method: req.method,
    headers: req.headers,
    path: req.url,
    agent: agent,
    pipe: true
  };
  if (typeof env.proxyOpts === 'object') {
    Object.keys(env.proxyOpts).forEach(function(k) {
      opts[k] = env.proxyOpts[k];
    });
  }

  var request = http.request(opts, function(response) {

    Object.keys(response.headers).forEach(function(header) {
      res.setHeader(header, response.headers[header]);
    });

    res.statusCode = response.statusCode;

    if (!opts.pipe) {
      var body = null;
      var buf = [];
      var len = 0;

      response.on('readable', function() {
        var chunk;

        while ((chunk = response.read()) != null) {
          buf.push(chunk);
          len += chunk.length;
        }

        if (!buf.length) {
          return;
        }

        body = new Buffer(len);
        var i = 0;
        buf.forEach(function(chunk) {
          chunk.copy(body, i, 0, chunk.length);
          i += chunk.length;
        });
      });

      response.on('end', function() {
        env.response.body = body;
        next(env);
      });
    } else {
      env.response.body = response;
      next(env);
    }
  }).on('error', function(err) {
    env.response.statusCode = 502;
    return next(env);
  });

  if (req.body) {
    request.end(req.body);
  } else {
    req.pipe(request);
  }
};
<|endoftext|>


<commit_before>'use strict';

/*
 * HyperSwitch request dispatcher and general shared per-request state namespace
 */

var P = require('bluebird');
var utils = require('./utils');
var HTTPError = require('./exports').HTTPError;
var preq = require('preq');
var swaggerUI = require('./swaggerUI');

/**
 * Create a uniform but shallow request object copy with sane defaults. This
 * keeps code dealing with this request monomorphic (good for perf), and
 * avoids subtle bugs when requests shared between recursive requests are
 * mutated in another control branch. At the very minimum, we are mutating the
 *
 * @param req original request object
 * @returns a shallow copy of a provided requests
 */
function cloneRequest(req) {
    var newReq = Object.assign({}, req);
    newReq.url = undefined;
    newReq.uri = req.uri || req.url || null;
    newReq.method = req.method || 'get';
    newReq.headers = req.headers || {};
    newReq.query = req.query || {};
    newReq.body = req.body !== undefined ? req.body : null;
    newReq.params = req.params || {};
    return newReq;
}

function HyperSwitch(options, req, parOptions) {
    if (options && options.constructor === HyperSwitch) {
        // Child instance
        var par = options;
        parOptions = parOptions || {};
        this.log = parOptions.log || par.log;
        this.metrics = parOptions.metrics || par.metrics;
        this.ratelimiter = parOptions.ratelimiter || par.ratelimiter;
        this.reqId = par.reqId ||
            req && req.headers && req.headers['x-request-id'] ||
            utils.generateRequestId();

        this._parent = par;
        // Remember the request that led to this child instance at each level, so
        // that we can provide nice error reporting and tracing.
        this._req = req;
        this._recursionDepth = par._recursionDepth + 1;
        this._priv = par._priv;
        this.config = this._priv.options.conf;
        this._rootReq = par._rootReq || req;
        this._rootReq.headers = this._rootReq.headers || {};
        this._requestFilters = par._requestFilters;
        this._subRequestFilters = par._subRequestFilters;
        this.ctx = par.ctx || {
            headers: {
                'user-agent': req.headers['user-agent'],
                'x-client-ip': req.headers['x-client-ip'],
            }
        };
    } else {
        // Brand new instance
        this.log = options.log; // Logging method
        this.metrics = options.metrics;
        this.ratelimiter = options.ratelimiter;
        this.reqId = null;

        // Private
        this._parent = null;
        this._req = null;
        this._recursionDepth = 0;

        options.maxDepth = options.maxDepth || 10;

        // Private state, shared with child instances
        this._priv = {
            options: options,
            router: options.router
        };

        this.config = options.conf;
        this.config.user_agent = this.config.user_agent || 'HyperSwitch';
        this._rootReq = null;
        this._requestFilters = [];
        this._subRequestFilters = [];
        this.ctx = parOptions && parOptions.ctx || null;
    }
}

// Sets the request id for this instance and adds it to
// the request header, if defined
HyperSwitch.prototype.setRequestId = function(req) {

    req.headers = req.headers || {};
    if (req.headers['x-request-id']) {
        return;
    }
    req.headers['x-request-id'] = this.reqId;

};

// Make a child instance
HyperSwitch.prototype.makeChild = function(req, options) {
    return new HyperSwitch(this, req, options);
};

// A default listing handler for URIs that end in / and don't have any
// handlers associated with it otherwise.
HyperSwitch.prototype.defaultListingHandler = function(match, hyper, req) {
    var rq = req.query;
    if (rq.spec !== undefined
            && match.value.specRoot && !match.value.specRoot['x-listing']) {
        var spec = Object.assign({}, match.value.specRoot);

        if (req.params.domain === req.headers.host.replace(/:[0-9]+$/, '')) {
            // This is a host-based request. Set an appropriate base path.
            spec.basePath = spec['x-host-basePath'] || spec.basePath;
        }

        return P.resolve({
            status: 200,
            body: spec
        });
    } else if (rq.path ||
        (match.value.specRoot
            && !match.value.specRoot['x-listing']
            && match.value.specRoot.basePath === req.uri.toString().replace(/\/$/, '')
            && /\btext\/html\b/.test(req.headers.accept))) {
        // Return swagger UI & load spec from /?spec
        if (!req.query.path) {
            req.query.path = '/index.html';
        }
        return swaggerUI(hyper, req);
    } else if (/\btext\/html\b/.test(req.headers.accept)
            && (!match.value.specRoot || match.value.specRoot['x-listing'])) {
        // Browser request and above api level
        req.query.path = '/index.html';
        var html = '
'
                    + '
Wikimedia REST API
'
                    + '
APIs:
'
                    + '

'
                    + req.params._ls.filter(function(item) {
                            // TODO: This will filter out everything called `sys`,
                            // not only {api:sys} elements
                            return item !== 'sys';
                        })
                        .map(function(api) {
                        return '
' + api + '
';
                    }).join('\n')
                    + '
';
        html += "
JSON listing

To retrieve a regular JSON listing, you can either "
            + "omit the Accept header, or send one that does not contain "
            + "text/html.

";

        return swaggerUI(hyper, req)
        .then(function(res) {
            res.body = res.body
                .replace(/window\.swaggerUi\.load\(\);/, '')
                .replace(/
/, html);
            return res;
        });
    } else {
        // Plain listing
        return P.resolve({
            status: 200,
            body: {
                items: req.params._ls.filter(function(item) {
                    // TODO: This will filter out everything called `sys`,
                    // not only {api:sys} elements
                    return item !== 'sys';
                })
            }
        });
    }
};

HyperSwitch.prototype._isSysRequest = function(req) {
    return ((req.params && req.params.api === 'sys')
        // TODO: Remove once params.api is reliable
            || (req.uri.path && req.uri.path.length > 1 && req.uri.path[1] === 'sys'));
};

/**
 * Checks if the maximum recursion depth has been exceeded by the request.
 * If yes, the 500 error is thrown, othervise this is a no-op
 *
 * @param {Object} req - a current request object
 * @private
 */
HyperSwitch.prototype._checkMaxRecursionDepth = function(req) {
    if (this._recursionDepth > this._priv.options.maxDepth) {
        var parents = [];
        var rb = this._parent;
        while (rb) {
            parents.push(rb._req);
            rb = rb._parent;
        }
        throw new HTTPError({
            status: 500,
            body: {
                type: 'server_error#request_recursion_depth_exceeded',
                title: 'HyperSwitch request recursion depth exceeded.',
                parents: parents,
                depth: this._recursionDepth
            }
        });
    }
};

/**
 * Protects /sys APIs from the direct access.
 *
 * @param {Object} req - an original request
 * @private
 */
HyperSwitch.prototype._checkInternalApiRequest = function(req) {
    if (this._recursionDepth === 0 && this._isSysRequest(req)) {
        throw new HTTPError({
            status: 403,
            body: {
                type: 'forbidden#sys',
                title: 'Access to the /sys hierarchy is restricted to system users.'
            }
        });
    }
};

HyperSwitch.prototype._createFilteredHandler = function(handler, filters, specInfo) {
    if (!filters || !filters.length) {
        return handler;
    }
    var filterIdx = 0;
    return function handlerWrapper(hyper, req) {
        if (filters && filterIdx < filters.length) {
            var filter = filters[filterIdx];
            filterIdx++;

            if (typeof filter === 'function') {
                return filter(hyper, req, handlerWrapper, filter.options, specInfo);
            }

            if (filter.method
                    && filter.method !== req.method
                    && !(filter.method === 'get' && req.method === 'head')) {
                return handlerWrapper(hyper, req);
            }

            return filter.filter(hyper, req, handlerWrapper, filter.options, specInfo);
        } else {
            return P.method(handler)(hyper, req);
        }
    };
};

HyperSwitch.prototype.request = function(req, options) {
    if (req.method) {
        req.method = req.method.toLowerCase();
    }
    return this._filteredRequest(req, options);
};

HyperSwitch.prototype._filteredRequest = function(req, options) {
    this._checkMaxRecursionDepth(req);
    // Make sure we have a sane & uniform request object that doesn't change
    // (at least at the top level) under our feet.
    var childReq = cloneRequest(req);
    return this._createFilteredHandler(function(hyper, childReq) {
        return hyper._request(childReq, options);
    }, this._recursionDepth === 0 ? this._requestFilters : this._subRequestFilters)(this, childReq);
};

// Process one request
HyperSwitch.prototype._request = function(req, options) {
    var self = this;

    // Look up the route in the tree.
    var match = this._priv.router.route(req.uri);
    var handler;
    if (match) {
        req.params = match.params;
        self._checkInternalApiRequest(req);

        // Find a handler.
        var methods = match.value && match.value.methods || {};
        handler = methods[req.method] || methods.all;
        if (!handler
                && (req.method === 'head'
                    || self._rootReq && self._rootReq.method === 'head')) {
            handler = methods && methods.get;
        }

        if (!handler
                && req.method === 'get'
                && req.uri.path[req.uri.path.length - 1] === '') {
            // A GET for an URL that ends with /: return a default listing
            if (!match.value) { match.value = {}; }
            if (!match.value.path) { match.value.path = '_defaultListingHandler'; }
            handler = function(hyper, req) {
                return self.defaultListingHandler(match, hyper, req);
            };
        }
    }

    if (handler) {
        // Prepare to call the handler with a child HyperSwitch instance
        var childHyperSwitch = this.makeChild(req, options);
        var reqHandler = childHyperSwitch._createFilteredHandler(handler, match.filters, {
            path: match.value.path,
            spec: handler.spec
        });
        // This is a hack. Pure P.try get's executed on this tick, but we wanna
        // wrap it in metrics and access checks and start execution only afterwards.
        // This will go away when filters are completed.
        var reqHandlerPromise = P.resolve()
        .then(function() {
            return reqHandler(childHyperSwitch, req);
        });

        return reqHandlerPromise
        .then(function(res) {
            childHyperSwitch.log('trace/hyper/response', {
                req: req,
                res: res,
                request_id: childHyperSwitch.reqId
            });

            if (!res) {
                throw new HTTPError({
                    status: 500,
                    body: {
                        type: 'server_error#empty_response',
                        title: 'Server Error',
                        description: 'Empty response received from a backend service'
                    }
                });
            } else if (!(res.status >= 100 && res.status < 400) && !(res instanceof Error)) {
                var err = new HTTPError(res);
                if (res.body && res.body.stack) { err.stack = res.body.stack; }
                err.innerBody = res.body && JSON.stringify(res.body).substr(0, 200);
                err.internalReq = req && {
                        method: req.method,
                        headers: req.headers,
                        query: req.query,
                        body: req.body && JSON.stringify(req.body).substr(0, 200)
                    };
                throw err;
            } else {
                return res;
            }
        });
    } else {
        // No handler found.
        throw new HTTPError({
            status: 404,
            body: {
                type: 'not_found#route',
                title: 'Not found.',
                internalURI: req.uri,
                method: req.method,
                depth: self._recursionDepth
            }
        });
    }
};

// Generic parameter massaging:
// * If last parameter is an object, it is expected to be the request object.
// * If the first parameter is a string, it's expected to be the URL.
// * If the second parameter is a String or Buffer, it's expected to be a
//   resource body.
function makeRequest(uri, reqOrBody, method) {
    var req;
    if (uri.constructor === Object) {
        // Fast path
        req = uri;
        req.method = method;
        return req;
    } else if (reqOrBody && reqOrBody.constructor === Object) {
        req = reqOrBody;
        req.uri = uri;
        req.method = method;
    } else {
        req = {
            uri: uri,
            method: method,
            body: reqOrBody
        };
    }
    return req;
}

// Convenience wrappers
var methods = ['get', 'post', 'put', 'delete', 'head', 'options',
    'trace', 'connect', 'copy', 'move', 'purge', 'search'];
methods.forEach(function(method) {
    HyperSwitch.prototype[method] = function(uri, req) {
        return this._filteredRequest(makeRequest(uri, req, method));
    };
});

module.exports = HyperSwitch;
<commit_msg>Returned the dynamic basePath replacement<commit_after>'use strict';

/*
 * HyperSwitch request dispatcher and general shared per-request state namespace
 */

var P = require('bluebird');
var utils = require('./utils');
var HTTPError = require('./exports').HTTPError;
var preq = require('preq');
var swaggerUI = require('./swaggerUI');

/**
 * Create a uniform but shallow request object copy with sane defaults. This
 * keeps code dealing with this request monomorphic (good for perf), and
 * avoids subtle bugs when requests shared between recursive requests are
 * mutated in another control branch. At the very minimum, we are mutating the
 *
 * @param req original request object
 * @returns a shallow copy of a provided requests
 */
function cloneRequest(req) {
    var newReq = Object.assign({}, req);
    newReq.url = undefined;
    newReq.uri = req.uri || req.url || null;
    newReq.method = req.method || 'get';
    newReq.headers = req.headers || {};
    newReq.query = req.query || {};
    newReq.body = req.body !== undefined ? req.body : null;
    newReq.params = req.params || {};
    return newReq;
}

function HyperSwitch(options, req, parOptions) {
    if (options && options.constructor === HyperSwitch) {
        // Child instance
        var par = options;
        parOptions = parOptions || {};
        this.log = parOptions.log || par.log;
        this.metrics = parOptions.metrics || par.metrics;
        this.ratelimiter = parOptions.ratelimiter || par.ratelimiter;
        this.reqId = par.reqId ||
            req && req.headers && req.headers['x-request-id'] ||
            utils.generateRequestId();

        this._parent = par;
        // Remember the request that led to this child instance at each level, so
        // that we can provide nice error reporting and tracing.
        this._req = req;
        this._recursionDepth = par._recursionDepth + 1;
        this._priv = par._priv;
        this.config = this._priv.options.conf;
        this._rootReq = par._rootReq || req;
        this._rootReq.headers = this._rootReq.headers || {};
        this._requestFilters = par._requestFilters;
        this._subRequestFilters = par._subRequestFilters;
        this.ctx = par.ctx || {
            headers: {
                'user-agent': req.headers['user-agent'],
                'x-client-ip': req.headers['x-client-ip'],
            }
        };
    } else {
        // Brand new instance
        this.log = options.log; // Logging method
        this.metrics = options.metrics;
        this.ratelimiter = options.ratelimiter;
        this.reqId = null;

        // Private
        this._parent = null;
        this._req = null;
        this._recursionDepth = 0;

        options.maxDepth = options.maxDepth || 10;

        // Private state, shared with child instances
        this._priv = {
            options: options,
            router: options.router
        };

        this.config = options.conf;
        this.config.user_agent = this.config.user_agent || 'HyperSwitch';
        this._rootReq = null;
        this._requestFilters = [];
        this._subRequestFilters = [];
        this.ctx = parOptions && parOptions.ctx || null;
    }
}

// Sets the request id for this instance and adds it to
// the request header, if defined
HyperSwitch.prototype.setRequestId = function(req) {

    req.headers = req.headers || {};
    if (req.headers['x-request-id']) {
        return;
    }
    req.headers['x-request-id'] = this.reqId;

};

// Make a child instance
HyperSwitch.prototype.makeChild = function(req, options) {
    return new HyperSwitch(this, req, options);
};

// A default listing handler for URIs that end in / and don't have any
// handlers associated with it otherwise.
HyperSwitch.prototype.defaultListingHandler = function(match, hyper, req) {
    var rq = req.query;
    if (rq.spec !== undefined
            && match.value.specRoot && !match.value.specRoot['x-listing']) {
        var spec = Object.assign({}, match.value.specRoot, {
            // Set the base path dynamically
            basePath: req.uri.toString().replace(/\/$/, '')
        });

        if (req.params.domain === req.headers.host.replace(/:[0-9]+$/, '')) {
            // This is a host-based request. Set an appropriate base path.
            spec.basePath = spec['x-host-basePath'] || spec.basePath;
        }

        return P.resolve({
            status: 200,
            body: spec
        });
    } else if (rq.path ||
        (match.value.specRoot
            && !match.value.specRoot['x-listing']
            && match.value.specRoot.basePath === req.uri.toString().replace(/\/$/, '')
            && /\btext\/html\b/.test(req.headers.accept))) {
        // Return swagger UI & load spec from /?spec
        if (!req.query.path) {
            req.query.path = '/index.html';
        }
        return swaggerUI(hyper, req);
    } else if (/\btext\/html\b/.test(req.headers.accept)
            && (!match.value.specRoot || match.value.specRoot['x-listing'])) {
        // Browser request and above api level
        req.query.path = '/index.html';
        var html = '
'
                    + '
Wikimedia REST API
'
                    + '
APIs:
'
                    + '

'
                    + req.params._ls.filter(function(item) {
                            // TODO: This will filter out everything called `sys`,
                            // not only {api:sys} elements
                            return item !== 'sys';
                        })
                        .map(function(api) {
                        return '
' + api + '
';
                    }).join('\n')
                    + '
';
        html += "
JSON listing

To retrieve a regular JSON listing, you can either "
            + "omit the Accept header, or send one that does not contain "
            + "text/html.

";

        return swaggerUI(hyper, req)
        .then(function(res) {
            res.body = res.body
                .replace(/window\.swaggerUi\.load\(\);/, '')
                .replace(/
/, html);
            return res;
        });
    } else {
        // Plain listing
        return P.resolve({
            status: 200,
            body: {
                items: req.params._ls.filter(function(item) {
                    // TODO: This will filter out everything called `sys`,
                    // not only {api:sys} elements
                    return item !== 'sys';
                })
            }
        });
    }
};

HyperSwitch.prototype._isSysRequest = function(req) {
    return ((req.params && req.params.api === 'sys')
        // TODO: Remove once params.api is reliable
            || (req.uri.path && req.uri.path.length > 1 && req.uri.path[1] === 'sys'));
};

/**
 * Checks if the maximum recursion depth has been exceeded by the request.
 * If yes, the 500 error is thrown, othervise this is a no-op
 *
 * @param {Object} req - a current request object
 * @private
 */
HyperSwitch.prototype._checkMaxRecursionDepth = function(req) {
    if (this._recursionDepth > this._priv.options.maxDepth) {
        var parents = [];
        var rb = this._parent;
        while (rb) {
            parents.push(rb._req);
            rb = rb._parent;
        }
        throw new HTTPError({
            status: 500,
            body: {
                type: 'server_error#request_recursion_depth_exceeded',
                title: 'HyperSwitch request recursion depth exceeded.',
                parents: parents,
                depth: this._recursionDepth
            }
        });
    }
};

/**
 * Protects /sys APIs from the direct access.
 *
 * @param {Object} req - an original request
 * @private
 */
HyperSwitch.prototype._checkInternalApiRequest = function(req) {
    if (this._recursionDepth === 0 && this._isSysRequest(req)) {
        throw new HTTPError({
            status: 403,
            body: {
                type: 'forbidden#sys',
                title: 'Access to the /sys hierarchy is restricted to system users.'
            }
        });
    }
};

HyperSwitch.prototype._createFilteredHandler = function(handler, filters, specInfo) {
    if (!filters || !filters.length) {
        return handler;
    }
    var filterIdx = 0;
    return function handlerWrapper(hyper, req) {
        if (filters && filterIdx < filters.length) {
            var filter = filters[filterIdx];
            filterIdx++;

            if (typeof filter === 'function') {
                return filter(hyper, req, handlerWrapper, filter.options, specInfo);
            }

            if (filter.method
                    && filter.method !== req.method
                    && !(filter.method === 'get' && req.method === 'head')) {
                return handlerWrapper(hyper, req);
            }

            return filter.filter(hyper, req, handlerWrapper, filter.options, specInfo);
        } else {
            return P.method(handler)(hyper, req);
        }
    };
};

HyperSwitch.prototype.request = function(req, options) {
    if (req.method) {
        req.method = req.method.toLowerCase();
    }
    return this._filteredRequest(req, options);
};

HyperSwitch.prototype._filteredRequest = function(req, options) {
    this._checkMaxRecursionDepth(req);
    // Make sure we have a sane & uniform request object that doesn't change
    // (at least at the top level) under our feet.
    var childReq = cloneRequest(req);
    return this._createFilteredHandler(function(hyper, childReq) {
        return hyper._request(childReq, options);
    }, this._recursionDepth === 0 ? this._requestFilters : this._subRequestFilters)(this, childReq);
};

// Process one request
HyperSwitch.prototype._request = function(req, options) {
    var self = this;

    // Look up the route in the tree.
    var match = this._priv.router.route(req.uri);
    var handler;
    if (match) {
        req.params = match.params;
        self._checkInternalApiRequest(req);

        // Find a handler.
        var methods = match.value && match.value.methods || {};
        handler = methods[req.method] || methods.all;
        if (!handler
                && (req.method === 'head'
                    || self._rootReq && self._rootReq.method === 'head')) {
            handler = methods && methods.get;
        }

        if (!handler
                && req.method === 'get'
                && req.uri.path[req.uri.path.length - 1] === '') {
            // A GET for an URL that ends with /: return a default listing
            if (!match.value) { match.value = {}; }
            if (!match.value.path) { match.value.path = '_defaultListingHandler'; }
            handler = function(hyper, req) {
                return self.defaultListingHandler(match, hyper, req);
            };
        }
    }

    if (handler) {
        // Prepare to call the handler with a child HyperSwitch instance
        var childHyperSwitch = this.makeChild(req, options);
        var reqHandler = childHyperSwitch._createFilteredHandler(handler, match.filters, {
            path: match.value.path,
            spec: handler.spec
        });
        // This is a hack. Pure P.try get's executed on this tick, but we wanna
        // wrap it in metrics and access checks and start execution only afterwards.
        // This will go away when filters are completed.
        var reqHandlerPromise = P.resolve()
        .then(function() {
            return reqHandler(childHyperSwitch, req);
        });

        return reqHandlerPromise
        .then(function(res) {
            childHyperSwitch.log('trace/hyper/response', {
                req: req,
                res: res,
                request_id: childHyperSwitch.reqId
            });

            if (!res) {
                throw new HTTPError({
                    status: 500,
                    body: {
                        type: 'server_error#empty_response',
                        title: 'Server Error',
                        description: 'Empty response received from a backend service'
                    }
                });
            } else if (!(res.status >= 100 && res.status < 400) && !(res instanceof Error)) {
                var err = new HTTPError(res);
                if (res.body && res.body.stack) { err.stack = res.body.stack; }
                err.innerBody = res.body && JSON.stringify(res.body).substr(0, 200);
                err.internalReq = req && {
                        method: req.method,
                        headers: req.headers,
                        query: req.query,
                        body: req.body && JSON.stringify(req.body).substr(0, 200)
                    };
                throw err;
            } else {
                return res;
            }
        });
    } else {
        // No handler found.
        throw new HTTPError({
            status: 404,
            body: {
                type: 'not_found#route',
                title: 'Not found.',
                internalURI: req.uri,
                method: req.method,
                depth: self._recursionDepth
            }
        });
    }
};

// Generic parameter massaging:
// * If last parameter is an object, it is expected to be the request object.
// * If the first parameter is a string, it's expected to be the URL.
// * If the second parameter is a String or Buffer, it's expected to be a
//   resource body.
function makeRequest(uri, reqOrBody, method) {
    var req;
    if (uri.constructor === Object) {
        // Fast path
        req = uri;
        req.method = method;
        return req;
    } else if (reqOrBody && reqOrBody.constructor === Object) {
        req = reqOrBody;
        req.uri = uri;
        req.method = method;
    } else {
        req = {
            uri: uri,
            method: method,
            body: reqOrBody
        };
    }
    return req;
}

// Convenience wrappers
var methods = ['get', 'post', 'put', 'delete', 'head', 'options',
    'trace', 'connect', 'copy', 'move', 'purge', 'search'];
methods.forEach(function(method) {
    HyperSwitch.prototype[method] = function(uri, req) {
        return this._filteredRequest(makeRequest(uri, req, method));
    };
});

module.exports = HyperSwitch;
<|endoftext|>


<commit_before>/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

const invariant = require('invariant');
const deprecated = require('./deprecated.es6');
const Request = require('./request.es6');

/**
 * @typedef {object} request
 * @property {string} method
 * @property {string} route - string like /abc/:abc
 * @property {string} params
 * @property {object} [body]
 * @property {object} headers
 */

/**
 * @typedef {object} response
 * @property {Error} error - an error which occured during req or res
 * @property {object} body - content received from server (parsed)
 * @property {object} headers - set additional request headers
 * @property {number} status - http status code; 0 on failure
 */

/**
 * XHR wrapper for same-domain requests with Content-Type: application/json
 *
 * @param {Object} request
 * @param {Function} callback function
 * @return {Object} request object that is cancelable
 */
export default function implore(request, callback) {
	const response = {
		error: null
	};

	const {withCredentials=false, headers={}} = request;
	const xhr = new XMLHttpRequest();

	invariant(
		request,
		'implore requires a `request` argument'
	);

	invariant(
		typeof request.route === 'string',
		'implore requires parameter `route` to be a string'
	);

	invariant(
		typeof request.method === 'string',
		'implore requires parameter `method` to be a string'
	);

	invariant(
		typeof withCredentials === 'boolean',
		'implore requires parameter `withCredentials` to be a boolean'
	);

	invariant(
		typeof headers === 'object',
		'implore requires parameter `headers` to be an object'
	);

	xhr.open(request.method, getURLFromRequest(request));
	xhr.withCredentials = request.withCredentials;

	let contentTypeSetByUser = false;

	if (request.contentType) {
		deprecated(true, 'contentType in createRequest', 'headers');
		xhr.setRequestHeader('Content-Type', request.contentType);
		contentTypeSetByUser = true;
	}

	Object.keys(headers).forEach((header) => {
		if (header === 'Content-Type') {
			contentTypeSetByUser = true;
		}
		xhr.setRequestHeader(header, headers[header]);
	});

	if (!contentTypeSetByUser) {
		switch (request.method) {
			case 'POST':
			case 'PUT':
			case 'PATCH':
				xhr.setRequestHeader('Content-Type', 'application/json');
				break;
		}
	}

	xhr.onabort = function onabort() {
		callback({
			request,
			response: {
				fluxthisAborted: true
			}
		});
	};

	xhr.onreadystatechange = function onreadystatechange() {
		if (xhr.readyState === 4 && xhr.status !== 0) {
			response.body = xhr.responseText;
			response.status = xhr.status;
			response.type = xhr.getResponseHeader('Content-Type');

			if (/application\/json/.test(response.type)) {
				try {
					response.body = JSON.parse(response.body);
				} catch (err) {
					response.status = xhr.status || 0;
					response.error = err;
				}
			}

			callback({
				request,
				response
			});
		}
	};

	try {
		if (request.body) {
			if (typeof request.body === 'string') {
				xhr.send(request.body);
			} else {
				xhr.send(JSON.stringify(request.body));
			}
		}
		else {
			xhr.send();
		}
	}
	catch (err) {
		response.body = {};
		response.status = 0;
		response.error = err;

		callback({
			request,
			response
		});

		return null;
	}

	return new Request(xhr);
}

implore.get = function get(options) {
	options.method = 'GET';
	return implore(options);
};

implore.post = function post(options) {
	options.method = 'POST';
	return implore(options);
};

implore.put = function put(options) {
	options.method = 'PUT';
	return implore(options);
};

implore.delete = function httpDelete(options) {
	options.method = 'DELETE';
	return implore(options);
};

/**
 * Combine the route/params/query of a request into a complete URL
 *
 * @param {request} request
 * @param {object|array} request.query
 * @return {string} url
 */
function getURLFromRequest(request) {
	const queryString = makeQueryString(request.query || {});
	let formatted = request.route;
	let name;
	let value;
	let regexp;

	for (name in request.params) {
		if (request.params.hasOwnProperty(name)) {
			value = request.params[name];
			regexp = new RegExp(':' + name + '(?=(\\\/|$))');
			formatted = formatted.replace(regexp, value);
		}
	}

	return formatted + (queryString ? '?' + queryString : '');
}

/**
 * Take a simple object and turn it into a queryString, recursively.
 *
 * @param {object} obj - query object
 * @param {string} prefix - used in recursive calls to keep track of the parent
 * @return {string} queryString without the '?''
 */
function makeQueryString(obj, prefix='') {
	const str = [];
	let prop;
	let key;
	let value;

	for (prop in obj) {
		if (obj.hasOwnProperty(prop)) {
			key = prefix ?
				prefix + '[' + prop + ']' :
				prop;
			value = obj[prop];
			str.push(typeof value === 'object' ?
				makeQueryString(value, key) :
				encodeURIComponent(key) + '=' + encodeURIComponent(value));
		}
	}

	return str.join('&');
}
<commit_msg>Allow empty body response with application/json content type<commit_after>/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

const invariant = require('invariant');
const deprecated = require('./deprecated.es6');
const Request = require('./request.es6');

/**
 * @typedef {object} request
 * @property {string} method
 * @property {string} route - string like /abc/:abc
 * @property {string} params
 * @property {object} [body]
 * @property {object} headers
 */

/**
 * @typedef {object} response
 * @property {Error} error - an error which occured during req or res
 * @property {object} body - content received from server (parsed)
 * @property {object} headers - set additional request headers
 * @property {number} status - http status code; 0 on failure
 */

/**
 * XHR wrapper for same-domain requests with Content-Type: application/json
 *
 * @param {Object} request
 * @param {Function} callback function
 * @return {Object} request object that is cancelable
 */
export default function implore(request, callback) {
	const response = {
		error: null
	};

	const {withCredentials=false, headers={}} = request;
	const xhr = new XMLHttpRequest();

	invariant(
		request,
		'implore requires a `request` argument'
	);

	invariant(
		typeof request.route === 'string',
		'implore requires parameter `route` to be a string'
	);

	invariant(
		typeof request.method === 'string',
		'implore requires parameter `method` to be a string'
	);

	invariant(
		typeof withCredentials === 'boolean',
		'implore requires parameter `withCredentials` to be a boolean'
	);

	invariant(
		typeof headers === 'object',
		'implore requires parameter `headers` to be an object'
	);

	xhr.open(request.method, getURLFromRequest(request));
	xhr.withCredentials = request.withCredentials;

	let contentTypeSetByUser = false;

	if (request.contentType) {
		deprecated(true, 'contentType in createRequest', 'headers');
		xhr.setRequestHeader('Content-Type', request.contentType);
		contentTypeSetByUser = true;
	}

	Object.keys(headers).forEach((header) => {
		if (header === 'Content-Type') {
			contentTypeSetByUser = true;
		}
		xhr.setRequestHeader(header, headers[header]);
	});

	if (!contentTypeSetByUser) {
		switch (request.method) {
			case 'POST':
			case 'PUT':
			case 'PATCH':
				xhr.setRequestHeader('Content-Type', 'application/json');
				break;
		}
	}

	xhr.onabort = function onabort() {
		callback({
			request,
			response: {
				fluxthisAborted: true
			}
		});
	};

	xhr.onreadystatechange = function onreadystatechange() {
		if (xhr.readyState === 4 && xhr.status !== 0) {
			response.body = xhr.responseText;
			response.status = xhr.status;
			response.type = xhr.getResponseHeader('Content-Type');

			if (/application\/json/.test(response.type)) {
				if (response.body && response.status !== 204) {
					try {
						response.body = JSON.parse(response.body);
					} catch (err) {
						response.status = xhr.status || 0;
						response.error = err;
					}
				}
			}

			callback({
				request,
				response
			});
		}
	};

	try {
		if (request.body) {
			if (typeof request.body === 'string') {
				xhr.send(request.body);
			} else {
				xhr.send(JSON.stringify(request.body));
			}
		}
		else {
			xhr.send();
		}
	}
	catch (err) {
		response.body = {};
		response.status = 0;
		response.error = err;

		callback({
			request,
			response
		});

		return null;
	}

	return new Request(xhr);
}

implore.get = function get(options) {
	options.method = 'GET';
	return implore(options);
};

implore.post = function post(options) {
	options.method = 'POST';
	return implore(options);
};

implore.put = function put(options) {
	options.method = 'PUT';
	return implore(options);
};

implore.delete = function httpDelete(options) {
	options.method = 'DELETE';
	return implore(options);
};

/**
 * Combine the route/params/query of a request into a complete URL
 *
 * @param {request} request
 * @param {object|array} request.query
 * @return {string} url
 */
function getURLFromRequest(request) {
	const queryString = makeQueryString(request.query || {});
	let formatted = request.route;
	let name;
	let value;
	let regexp;

	for (name in request.params) {
		if (request.params.hasOwnProperty(name)) {
			value = request.params[name];
			regexp = new RegExp(':' + name + '(?=(\\\/|$))');
			formatted = formatted.replace(regexp, value);
		}
	}

	return formatted + (queryString ? '?' + queryString : '');
}

/**
 * Take a simple object and turn it into a queryString, recursively.
 *
 * @param {object} obj - query object
 * @param {string} prefix - used in recursive calls to keep track of the parent
 * @return {string} queryString without the '?''
 */
function makeQueryString(obj, prefix='') {
	const str = [];
	let prop;
	let key;
	let value;

	for (prop in obj) {
		if (obj.hasOwnProperty(prop)) {
			key = prefix ?
				prefix + '[' + prop + ']' :
				prop;
			value = obj[prop];
			str.push(typeof value === 'object' ?
				makeQueryString(value, key) :
				encodeURIComponent(key) + '=' + encodeURIComponent(value));
		}
	}

	return str.join('&');
}
<|endoftext|>


<commit_before>"use strict";

const stepStates = require('./import-step-states'),
        execSync = require('child_process').execSync,
         rfc6902 = require('rfc6902'),
            path = require('path'),
              fs = require('fs-extra');

/**
 * Processes metadata located in <pkg-name@version?.json> metadata file
 *
 * Steps in execution order
 * 0. register
 *
 * 1. applyPatches
 * 2. registerDependencies
 * 3. registerBundles
 * 4. saveProject
 * 5. installTasks
 * 6. executeScripts
 */
module.exports = class {

    constructor() { }

    /**
     * Register this class with ImportEngine
     *
     * @param engine ImportEngine ImportEngine instance
     * @param options any given CLI arguments
     */
    register(engine, options) {
        this.engine = engine;
        this.cliParams = options;

        this.bundles = this.engine.projectConfig.build.bundles;
        if (this.bundles.length === 0) {
            throw new Error("aurelia.json: bundles section is missing.");
        }

        this.bundleName = this.cliParams.bundle || 'vendor-bundle.js';
        this.bundle = this.bundles.find(item => item.name === this.bundleName);
    }

    /**
     * Apply RFC6902 standard JSON patches against aurelia.json
     *
     * @return ImportStepState | void
     */
    applyPatches() {
        let project = this.engine.projectConfig,
            config = this.engine.definitionConfig;

        // patch aurelia.json settings with overrides
        if (this.cliParams.action === 'install') {
            if (config && config.patches && config.patches.length > 0) {

                let result = rfc6902.applyPatch(project, config.patches),
                    errors = result.filter(err => err !== null);

                if (errors.length > 0) {
                    let msg = errors.map(e => e.message).join(', ');
                    throw new Error(`An error occurred while applying custom settings.\nErrors: ${msg}`);
                }

                this.writeLine(`[INFO] Custom project settings have been applied to aurelia.json.`);
            }
        }
    }

    /**
     * Register dependencies within aurelia.json bundle section
     * @return ImportStepState | void
     */
    registerDependencies() {
        if (!this.bundle) {
            this.writeLine(`[INFO] Bundle '${this.bundleName}' could not be found. Looking for default bundles...`);

            // There are 2 sections by default, second is usually the vendor-bundle.js
            // Although, some developers prefer to merge everything into a single bundle
            let index = this.bundles.length > 1 ? 1 : 0;
            this.bundle = this.bundles[index];

            // this should not be reached ever, but never say never :)
            if (!bundle) {
                throw new Error('Default bundle could not be found either. Check aurelia.json configuration.');
            }

            this.bundleName = bundle.name;
        }

        if (!this.bundle.dependencies) {
            if (this.cliParams.action === 'uninstall') {
                this.writeLine(`[INFO] No dependencies found in ${this.bundleName}.`);
            }
            this.bundle.dependencies = [];
        }

        let config = this.engine.definitionConfig;
        if (config.dependencies && config.dependencies.length > 0) {
            this.writeLine(`[INFO] Bundle found: ${this.bundleName}. Configuring new dependencies in aurelia.json...`);
            for (let dep of config.dependencies) {
                let name = dep.name || dep,
                    check = this.bundle.dependencies.find(item => (item.name || item) === name);

                if (!check) {
                    if (this.cliParams.action === 'install') {
                        this.bundle.dependencies.push(dep);
                        this.writeLine(`[NEW] '${name}' has been configured.`);
                    }
                } else {
                    let i = this.bundle.dependencies.indexOf(check);

                    if (this.cliParams.action === 'install') {
                        if (this.cliParams.force) {
                            this.bundle.dependencies[i] = dep;
                            this.writeLine(`[MOD] '${name}' has been modified.`);
                        } else {
                            this.writeLine(`[SKIP] '${name}' has already been configured.`);
                        }
                    } else {
                        this.bundle.dependencies.splice(i, 1);
                        this.writeLine(`[DEL] '${name}' has been removed.`);
                    }
                }
            }
            this.writeLine(`[OK] ${this.bundleName} has been configured.\n`);
        }

    }

    /**
     * Register new bundle configuration(s) in aurelia.json
     * @return ImportStepState | void
     */
    registerBundles() {
        let config = this.engine.definitionConfig;
        if (config.bundles && config.bundles.length > 0) {
            this.writeLine(`[INFO] Additional bundles found. Configuring new bundles in aurelia.json...`);
            for (let newBundle of config.bundles) {
                let checkBundle = this.bundles.find(b => b.name === newBundle.name);
                if (!checkBundle) {
                    if (this.cliParams.action === 'install') {
                        this.bundles.push(newBundle);
                        this.writeLine(`[NEW] Bundle '${newBundle.name}' has been added.`);
                    }
                } else {
                    let i = this.bundles.indexOf(checkBundle);

                    if (this.cliParams.action === 'install') {
                        if (this.cliParams.force) {
                            this.bundles[i] = newBundle;
                            this.writeLine(`[MOD] Bundle '${newBundle.name}' has been modified.`);
                        } else {
                            this.writeLine(`[SKIP] Bundle '${newBundle.name}' has already been configured, no action taken. Use --force/f switch to override entire bundles.`);
                        }
                    } else {
                        this.bundles.splice(i, 1);
                        this.writeLine(`[DEL] Bundle '${newBundle.name}' has been removed.`);
                    }
                }
            }
        }

    }

    /**
     * Install custom aurelia-cli tasks to aurelia_project/tasks folder
     *
     * Capable of installing tasks provided by the npm package
     * currently being configured (e.g. ./node_modules/plugin/..)
     *
     * @return ImportStepState | void
     */
    installTasks() {
        let tasks = this.engine.definitionConfig.tasks ||[];
        if (tasks.length > 0) {
            this.writeLine(`[INFO] ${tasks.length} custom task(s) found. Copying to aurelia_project/tasks folder...`);
        }

        let projectFolder = 'aurelia_project/',
            project = this.engine.projectConfig,
            destFolder = `${projectFolder}tasks/`;

        for (let taskName of tasks) {
            // determinate transpiler to set correct file extension
            let filename = taskName + project.transpiler.fileExtension,
                destFile = destFolder + filename,
                source = null;

            // If file is missing, aborting importation process by throwing an Error
            try {
                // by default, search in installed package directory
                let pkgName = this.engine.options.pkg;
                pkgName = pkgName.indexOf('@') !== -1 ? pkgName.split('@')[0] : pkgName;
                source = path.resolve(__dirname, `../../${pkgName}/install/${taskName}`);
                fs.copySync(`${source}.js`, destFile);
            } catch (e) {
                // fallback to 'tasks' folder in aurelia-cli-pacman
                source = path.resolve(__dirname, `../tasks/${taskName}`);
                fs.copySync(`${source}.js`, destFile);
            }

            // task metadata is optional
            try {
                let fileInfo = fs.statSync(`${source}.json`);
                if (fileInfo.isFile()) {
                    fs.copySync(`${source}.json`, `${dest + taskName}.json`);
                }
            } catch (err) {
                if (err.code !== 'ENOENT') {
                    this.writeLine(`[ERR] ${source}.json: `, err);
                }
            }

            this.writeLine(`[INFO] Custom task: ${taskName} has been installed.`);
        }

        this.writeLine(`[OK] Custom tasks have been installed successfully.\n`);
    }

    /**
     * Execute custom defined scripts
     * E.g. execute previously installed tasks (au <task>
     *      execute any other node script
     *
     * @return ImportStepState | void
     */
    executeScripts() {
        let config = this.engine.definitionConfig;

        try {
            let checkScripts = config.scripts ? config.scripts[this.cliParams.action] || null : null;

            if (checkScripts && checkScripts.length > 0) {
                this.writeLine(`[INFO] Additional scripts found. Executing...`);
                for (let script of checkScripts) {
                    this.writeLine(`[EXE] Executing: ${script}`);
                    execSync(script);
                }
            }
            this.writeLine(`[OK] Additional scripts finished successfully.`);
            this.writeLine(`\n[OK] ${this.cliParams.pkg} has been configured successfully.`);
        } catch (e) {
            this.writeLine(`[ERR] An error occurred during script execution.`, e.message);
        }
    }

    /**
     * Backup original file, save changes to aurelia.json
     *
     * @return ImportStepState | void
     */
    saveProject() {
        let aureliaProjectFile = 'aurelia_project/aurelia.json',
            aureliaProjectFileBackup = `${aureliaProjectFile}.${Date.now()}.bak`;

        this.writeLine('[INFO] Saving changes to aurelia.json file...');
        fs.copySync(aureliaProjectFile, aureliaProjectFileBackup);

        this.writeLine(`[INFO] Backup of aurelia.json has been created: ${aureliaProjectFileBackup}`);

        fs.writeJsonSync(aureliaProjectFile, this.engine.projectConfig);
        this.writeLine(`[OK] ${aureliaProjectFile} has been updated.\n`);
    }

    /**
     * Output messages controlled by --quiet option
     *
     * @param msg string message
     * @param err any additional data
     * @void
     */
    writeLine(msg, err) {
        if (this.cliParams.quiet !== true) {
            console.log(msg, err || '');
        }
    }
};
<commit_msg>Fix: task lookup error<commit_after>"use strict";

const stepStates = require('./import-step-states'),
        execSync = require('child_process').execSync,
         rfc6902 = require('rfc6902'),
            path = require('path'),
              fs = require('fs-extra');

/**
 * Processes metadata located in <pkg-name@version?.json> metadata file
 *
 * Steps in execution order
 * 0. register
 *
 * 1. applyPatches
 * 2. registerDependencies
 * 3. registerBundles
 * 4. saveProject
 * 5. installTasks
 * 6. executeScripts
 */
module.exports = class {

    constructor() { }

    /**
     * Register this class with ImportEngine
     *
     * @param engine ImportEngine ImportEngine instance
     * @param options any given CLI arguments
     */
    register(engine, options) {
        this.engine = engine;
        this.cliParams = options;

        this.bundles = this.engine.projectConfig.build.bundles;
        if (this.bundles.length === 0) {
            throw new Error("aurelia.json: bundles section is missing.");
        }

        this.bundleName = this.cliParams.bundle || 'vendor-bundle.js';
        this.bundle = this.bundles.find(item => item.name === this.bundleName);
    }

    /**
     * Apply RFC6902 standard JSON patches against aurelia.json
     *
     * @return ImportStepState | void
     */
    applyPatches() {
        let project = this.engine.projectConfig,
            config = this.engine.definitionConfig;

        // patch aurelia.json settings with overrides
        if (this.cliParams.action === 'install') {
            if (config && config.patches && config.patches.length > 0) {

                let result = rfc6902.applyPatch(project, config.patches),
                    errors = result.filter(err => err !== null);

                if (errors.length > 0) {
                    let msg = errors.map(e => e.message).join(', ');
                    throw new Error(`An error occurred while applying custom settings.\nErrors: ${msg}`);
                }

                this.writeLine(`[INFO] Custom project settings have been applied to aurelia.json.`);
            }
        }
    }

    /**
     * Register dependencies within aurelia.json bundle section
     * @return ImportStepState | void
     */
    registerDependencies() {
        if (!this.bundle) {
            this.writeLine(`[INFO] Bundle '${this.bundleName}' could not be found. Looking for default bundles...`);

            // There are 2 sections by default, second is usually the vendor-bundle.js
            // Although, some developers prefer to merge everything into a single bundle
            let index = this.bundles.length > 1 ? 1 : 0;
            this.bundle = this.bundles[index];

            // this should not be reached ever, but never say never :)
            if (!bundle) {
                throw new Error('Default bundle could not be found either. Check aurelia.json configuration.');
            }

            this.bundleName = bundle.name;
        }

        if (!this.bundle.dependencies) {
            if (this.cliParams.action === 'uninstall') {
                this.writeLine(`[INFO] No dependencies found in ${this.bundleName}.`);
            }
            this.bundle.dependencies = [];
        }

        let config = this.engine.definitionConfig;
        if (config.dependencies && config.dependencies.length > 0) {
            this.writeLine(`[INFO] Bundle found: ${this.bundleName}. Configuring new dependencies in aurelia.json...`);
            for (let dep of config.dependencies) {
                let name = dep.name || dep,
                    check = this.bundle.dependencies.find(item => (item.name || item) === name);

                if (!check) {
                    if (this.cliParams.action === 'install') {
                        this.bundle.dependencies.push(dep);
                        this.writeLine(`[NEW] '${name}' has been configured.`);
                    }
                } else {
                    let i = this.bundle.dependencies.indexOf(check);

                    if (this.cliParams.action === 'install') {
                        if (this.cliParams.force) {
                            this.bundle.dependencies[i] = dep;
                            this.writeLine(`[MOD] '${name}' has been modified.`);
                        } else {
                            this.writeLine(`[SKIP] '${name}' has already been configured.`);
                        }
                    } else {
                        this.bundle.dependencies.splice(i, 1);
                        this.writeLine(`[DEL] '${name}' has been removed.`);
                    }
                }
            }
            this.writeLine(`[OK] ${this.bundleName} has been configured.\n`);
        }

    }

    /**
     * Register new bundle configuration(s) in aurelia.json
     * @return ImportStepState | void
     */
    registerBundles() {
        let config = this.engine.definitionConfig;
        if (config.bundles && config.bundles.length > 0) {
            this.writeLine(`[INFO] Additional bundles found. Configuring new bundles in aurelia.json...`);
            for (let newBundle of config.bundles) {
                let checkBundle = this.bundles.find(b => b.name === newBundle.name);
                if (!checkBundle) {
                    if (this.cliParams.action === 'install') {
                        this.bundles.push(newBundle);
                        this.writeLine(`[NEW] Bundle '${newBundle.name}' has been added.`);
                    }
                } else {
                    let i = this.bundles.indexOf(checkBundle);

                    if (this.cliParams.action === 'install') {
                        if (this.cliParams.force) {
                            this.bundles[i] = newBundle;
                            this.writeLine(`[MOD] Bundle '${newBundle.name}' has been modified.`);
                        } else {
                            this.writeLine(`[SKIP] Bundle '${newBundle.name}' has already been configured, no action taken. Use --force/f switch to override entire bundles.`);
                        }
                    } else {
                        this.bundles.splice(i, 1);
                        this.writeLine(`[DEL] Bundle '${newBundle.name}' has been removed.`);
                    }
                }
            }
        }

    }

    /**
     * Install custom aurelia-cli tasks to aurelia_project/tasks folder
     *
     * Capable of installing tasks provided by the npm package
     * currently being configured (e.g. ./node_modules/plugin/..)
     *
     * @return ImportStepState | void
     */
    installTasks() {
        let tasks = this.engine.definitionConfig.tasks ||[];
        if (tasks.length > 0) {
            this.writeLine(`[INFO] ${tasks.length} custom task(s) found. Copying to aurelia_project/tasks folder...`);
        }

        let projectFolder = 'aurelia_project/',
            project = this.engine.projectConfig,
            destFolder = `${projectFolder}tasks/`;

        for (let taskName of tasks) {
            // determinate transpiler to set correct file extension
            let filename = taskName + project.transpiler.fileExtension,
                destFile = destFolder + filename,
                source = null;

            // If file is missing, aborting importation process by throwing an Error
            try {
                // by default, search in installed package directory
                let pkgName = this.cliParams.pkg;
                pkgName = pkgName.indexOf('@') !== -1 ? pkgName.split('@')[0] : pkgName;
                source = path.resolve(__dirname, `../../${pkgName}/install/${taskName}`);
                console.log(source);
                fs.copySync(`${source}.js`, destFile);
            } catch (e) {
                console.log(e);
                // fallback to 'tasks' folder in aurelia-cli-pacman
                source = path.resolve(__dirname, `../tasks/${taskName}`);
                fs.copySync(`${source}.js`, destFile);
            }

            // task metadata is optional
            try {
                let fileInfo = fs.statSync(`${source}.json`);
                if (fileInfo.isFile()) {
                    fs.copySync(`${source}.json`, `${dest + taskName}.json`);
                }
            } catch (err) {
                if (err.code !== 'ENOENT') {
                    this.writeLine(`[ERR] ${source}.json: `, err);
                }
            }

            this.writeLine(`[INFO] Custom task: ${taskName} has been installed.`);
        }

        this.writeLine(`[OK] Custom tasks have been installed successfully.\n`);
    }

    /**
     * Execute custom defined scripts
     * E.g. execute previously installed tasks (au <task>
     *      execute any other node script
     *
     * @return ImportStepState | void
     */
    executeScripts() {
        let config = this.engine.definitionConfig;

        try {
            let checkScripts = config.scripts ? config.scripts[this.cliParams.action] || null : null;

            if (checkScripts && checkScripts.length > 0) {
                this.writeLine(`[INFO] Additional scripts found. Executing...`);
                for (let script of checkScripts) {
                    this.writeLine(`[EXE] Executing: ${script}`);
                    execSync(script);
                }
            }
            this.writeLine(`[OK] Additional scripts finished successfully.`);
            this.writeLine(`\n[OK] ${this.cliParams.pkg} has been configured successfully.`);
        } catch (e) {
            this.writeLine(`[ERR] An error occurred during script execution.`, e.message);
        }
    }

    /**
     * Backup original file, save changes to aurelia.json
     *
     * @return ImportStepState | void
     */
    saveProject() {
        let aureliaProjectFile = 'aurelia_project/aurelia.json',
            aureliaProjectFileBackup = `${aureliaProjectFile}.${Date.now()}.bak`;

        this.writeLine('[INFO] Saving changes to aurelia.json file...');
        fs.copySync(aureliaProjectFile, aureliaProjectFileBackup);

        this.writeLine(`[INFO] Backup of aurelia.json has been created: ${aureliaProjectFileBackup}`);

        fs.writeJsonSync(aureliaProjectFile, this.engine.projectConfig);
        this.writeLine(`[OK] ${aureliaProjectFile} has been updated.\n`);
    }

    /**
     * Output messages controlled by --quiet option
     *
     * @param msg string message
     * @param err any additional data
     * @void
     */
    writeLine(msg, err) {
        if (this.cliParams.quiet !== true) {
            console.log(msg, err || '');
        }
    }
};
<|endoftext|>