<commit_before>#!/usr/bin/env node
"use strict";
const args = require("minimist")(process.argv.slice(2)),
      pkg = require("../package.json"),
      card = require("./card"),
      fs = require("fs-extra"),
      ejs = require("ejs"),
      git = require("simple-git")(),
      strftime = require("strftime"),
      open = require("open"),
      _ = require("underscore"),
      watch = require("./watch"),
      chalk = require("chalk");
const action = args._[0] // what are we trying to do?

function run(action, args, cb) {
  cb = cb || () => null;
  switch(action) {
    case "in":
      card.waltzIn().then(() => {
        console.log("Waltzed in. GO!");
        cb();
      }).catch(onError);
      break;

    case "out":
      card.waltzOut().then(() => {
        console.log("Waltzed out. Go work on something else.");
        cb();
      }).catch(onError);
      break;

    case "watch":
      watch.watchAndUpdateAccordingly(args);
      break;

    case "init":
      card.cardInit(args).then(() => {
        console.log(`Created ${chalk.cyan('`.timecard.json`')}. Start working with ${chalk.cyan('`waltz in`')}!`);
        cb();
      }).catch(onError);
      break;

    case "report":
    case "invoice":
      card.getCard().then((timecard) => {
        card.getReportTemplate(timecard.reportFormat || "default").then((contents) => {
          let ejs_data = card.getTimecardRenderDetails(timecard);
          let report = ejs.render(contents.toString(), ejs_data);
          if (args.print) {
            // push to stdout
            console.log(report);
            cb();
          } else {
            // write to disk
            fs.writeFile(args.file || "report.html", report, (err) => {
              if (err) {
                onError(err);
              } else {
                console.log("Wrote to", args.file || "report.html");
                cb();
              }
            });
          }
        }).catch(onError);
      }).catch(onError);
      break;

    case "ls":
    case "list":
    case "print":
      card.getCard().then((data) => {
        console.log(chalk.bold(chalk.green("Unpaid Hours") + " / " + chalk.red("Paid Hours") + ":"));
        let total_duration = totalDuration(data, (day, time) => {
          if (day.disabled) {
            console.log(`${chalk.red(day.date)}\t\t ${time.start} - ${time.end || "(no end)"}`)
          } else {
            console.log(`${chalk.green(day.date)}\t\t ${time.start} - ${time.end || "(no end)"}`)
          }
        });

        // the total time that it took, prettyprinted
        console.log(`About ${Math.floor(total_duration / 3600)} hours, ` +
          `${Math.floor(total_duration / 60) % 60} minutes, ` +
          `and ${total_duration % 60} seconds`
        )
        // if there is an hourly rate present, log out the total cost of the
        // project
        if (data.hourlyRate) {
          console.log(` = $${Math.round( 100 * (total_duration / 3600) * data.hourlyRate) / 100}`)
        }
      }).catch(onError);
      break;

    case "markpaid":
    case "paycheck":
      card.getCard().then((data) => {
        return Object.assign(data, {
          card: data.card.map((time) => {
            if (!time.disabled) {
              time.disabled = `${strftime(card.DAY_REPR)}\n${strftime(card.TIME_REPR)}`
            }
            return time;
          })
        });
      }).then(card.setCard).then(() => {
        console.log(chalk.cyan("All previous times are now disabled."));
      }).catch(onError);
      break;

    case "commit":
    case "push":
      card.indexCard().then((path) => {
        git.add([path], (err) => {
          if (err) {
            console.error(err);
          } else {
            git.commit(args.message || args.m || "Change work time via Waltz", [path], (err) => {
              if (err) {
                console.error(err);
              } else {
                console.log("Successfully logged changes!");
              }
            });
          }
        });
      });
      break;

    case "web":
    case "online":
    case "open":
      git.getRemotes(true, (err, all) => {
        if (err) {
          console.log(err);
        } else {
          let origin = all.find((i) => i.name === "origin");
          if (origin) {
            // try to match against a http or ssh url
            let data = origin.refs.push.match(/.+@.+\..+:(.+)\/(.+)\.git/) ||
                       origin.refs.push.match(/https?:\/\/.+\..+\/(.+)\/(.+)\.git/)
            if (data) {
              // form the url
              let url = `http://waltzapp.co/${data[1]}/${data[2]}`;
              console.log(url);
              if (!args.print) { open(url); }
            } else {
              console.log("Cannot do a regex match against", origin.refs.push);
            }
          } else {
            console.log("Cannot find a remote named origin to get a username and repositiory name from.");
          }
        }
      });
      break;

    default:
      if (args.help || args.h) {
        showHelp();
      } else if (args.version || args.v) {
        showVersion();
      } else {
        showNoCommand();
      }
      break;
  }
}
run(action, args);

function touchDotTimecard() {
  fs.mkdirpSync(`${process.env.HOME}/.timecard/templates`);
}

// given a timecard, calculate the total amount of time spent on the project.
function totalDuration(data, cbForEach) {
  return data.card.reduce((acc, day) => {
    return acc + day.times.reduce((acc, time) => {
      cbForEach && cbForEach(day, time);
      if (!day.disabled) {
        return acc + card.getDurationFor(day, time);
      } else {
        return acc;
      }
    }, 0);
  }, 0) / 1000 || 0;
}

// there was an error, somewhere
// catch it, and log it out.
function onError(error) {
  if (process.env.NODE_ENV !== "test") {
    console.log(chalk.red(`Oh no! There was an error!\n${error.toString()}`));
  } else {
    throw error;
  }
}

function showVersion() {
  console.log(`Waltz CLI version ${chalk.green(pkg.version)}`);
}

function showNoCommand() {
  card.echoLogo();
  showVersion();
  console.log(`No such command. Please run ${chalk.magenta("waltz --help")} for help.`);
}

function showHelp() {
  card.echoLogo();
  showVersion();
  console.log(`
Record your project development time.

Set up waltz with \`waltz init\`, then \`waltz in\` or \`waltz out\` to
record start or end time, respectively. Alternatively, use \`waltz watch\` to
manage recording your time automatically. To see a list of all times, run
\`waltz ls\`, and to generate a fancy-looking invoice, use \`waltz report\`.

Commands

  ---
  ${chalk.magenta("waltz init")}
  ---
  Create a new timecard in the current directory.
    \`--commit\` will fashion and make a new commit for this timecard.

  ---
  ${chalk.magenta("waltz in")}
  ---
  Clock in, starting a new time. This is typically run at the start of a work period.

  ---
  ${chalk.magenta("waltz out")}
  ---
  Clock out the current time. This is typically run at the end of a work period. If run with no unending times, it will error.

  ---
  ${chalk.magenta("waltz ls")} (aliased to \`waltz print\` and \`waltz list\`)
  ---
  List all times that are currently in the timecard.

  ---
  ${chalk.magenta("waltz paycheck")} (aliased to \`waltz markpaid\`)
  ---
  Marks all current times as paid.  After one is paid, run this command to reset the invoice total back to "zero", and start the next billing period. Times generated after this is run will still be unpaid, however.

  ---
  ${chalk.magenta("waltz report")} (aliased to \`waltz invoice\`)
  ---
  Generate an invoice and save in the current directory as \`report.html\`, by default.

    \`--print\` Print to stdout instead of writing to file.
    \`--file my_report.html\` Redirect the report to a different file.

  ---
  ${chalk.magenta("waltz watch")}
  ---
  This works by monitoring filesystem changes. When a file is updated, Waltz will record your start time, and after an expiry period, Waltz will record your end time. Because accuracy isn't guaranteed, if you need by-the-second times, use waltz in and waltz out.

    \`--expires 10\` Amount of minutes required of inactivity to
                 be considered "waltzed out". (default = 10 minutes)
    \`-- watchdir some/dir\` The root folder to watch for file changes
    \`-- verbose, -v\` Log all of the changes that waltz registers
    \`--quiet, -q\` Don't print anything to stdout (errors will still be logged)
    \`--ignore [Ii]gnore_me\` An ignore regex for files that shouldn't trigger a clock in or out.
                          Defaults to \`([\/\\]\.|node_modules|\.timecard\.json|\.git)\`

  ---
  ${chalk.magenta("web")}
  ---
  Open the Waltz online app in your default browser.
    \`--print\` will print the url instead of opening the url in the browser.

  ---
  ${chalk.magenta("commit")}
  ---
  Make a commit, using git, containing any changes made to the current timecard.
  Behind the scenes, this is equivelent to \`git add .timecard.json && git commit -m "Change work time via waltz"\`
    \`--message\` or \`-m\` Change the commit message

  ---
  ${chalk.magenta("Other")}
  ---
  -h, --help              Show this help message
  -v, --version           Show the current waltz cli version
  `.replace(/(`.*?`)/g, (match) => {
    // highlight all of the commands that are in backticks (remove them, and
    // change the color)
    return chalk.cyan(match.slice(1).slice(0, -1));
  }));
}

module.exports = {
  run,
};
<commit_msg>prefix commands with waltz<commit_after>#!/usr/bin/env node
"use strict";
const args = require("minimist")(process.argv.slice(2)),
      pkg = require("../package.json"),
      card = require("./card"),
      fs = require("fs-extra"),
      ejs = require("ejs"),
      git = require("simple-git")(),
      strftime = require("strftime"),
      open = require("open"),
      _ = require("underscore"),
      watch = require("./watch"),
      chalk = require("chalk");
const action = args._[0] // what are we trying to do?

function run(action, args, cb) {
  cb = cb || () => null;
  switch(action) {
    case "in":
      card.waltzIn().then(() => {
        console.log("Waltzed in. GO!");
        cb();
      }).catch(onError);
      break;

    case "out":
      card.waltzOut().then(() => {
        console.log("Waltzed out. Go work on something else.");
        cb();
      }).catch(onError);
      break;

    case "watch":
      watch.watchAndUpdateAccordingly(args);
      break;

    case "init":
      card.cardInit(args).then(() => {
        console.log(`Created ${chalk.cyan('`.timecard.json`')}. Start working with ${chalk.cyan('`waltz in`')}!`);
        cb();
      }).catch(onError);
      break;

    case "report":
    case "invoice":
      card.getCard().then((timecard) => {
        card.getReportTemplate(timecard.reportFormat || "default").then((contents) => {
          let ejs_data = card.getTimecardRenderDetails(timecard);
          let report = ejs.render(contents.toString(), ejs_data);
          if (args.print) {
            // push to stdout
            console.log(report);
            cb();
          } else {
            // write to disk
            fs.writeFile(args.file || "report.html", report, (err) => {
              if (err) {
                onError(err);
              } else {
                console.log("Wrote to", args.file || "report.html");
                cb();
              }
            });
          }
        }).catch(onError);
      }).catch(onError);
      break;

    case "ls":
    case "list":
    case "print":
      card.getCard().then((data) => {
        console.log(chalk.bold(chalk.green("Unpaid Hours") + " / " + chalk.red("Paid Hours") + ":"));
        let total_duration = totalDuration(data, (day, time) => {
          if (day.disabled) {
            console.log(`${chalk.red(day.date)}\t\t ${time.start} - ${time.end || "(no end)"}`)
          } else {
            console.log(`${chalk.green(day.date)}\t\t ${time.start} - ${time.end || "(no end)"}`)
          }
        });

        // the total time that it took, prettyprinted
        console.log(`About ${Math.floor(total_duration / 3600)} hours, ` +
          `${Math.floor(total_duration / 60) % 60} minutes, ` +
          `and ${total_duration % 60} seconds`
        )
        // if there is an hourly rate present, log out the total cost of the
        // project
        if (data.hourlyRate) {
          console.log(` = $${Math.round( 100 * (total_duration / 3600) * data.hourlyRate) / 100}`)
        }
      }).catch(onError);
      break;

    case "markpaid":
    case "paycheck":
      card.getCard().then((data) => {
        return Object.assign(data, {
          card: data.card.map((time) => {
            if (!time.disabled) {
              time.disabled = `${strftime(card.DAY_REPR)}\n${strftime(card.TIME_REPR)}`
            }
            return time;
          })
        });
      }).then(card.setCard).then(() => {
        console.log(chalk.cyan("All previous times are now disabled."));
      }).catch(onError);
      break;

    case "commit":
    case "push":
      card.indexCard().then((path) => {
        git.add([path], (err) => {
          if (err) {
            console.error(err);
          } else {
            git.commit(args.message || args.m || "Change work time via Waltz", [path], (err) => {
              if (err) {
                console.error(err);
              } else {
                console.log("Successfully logged changes!");
              }
            });
          }
        });
      });
      break;

    case "web":
    case "online":
    case "open":
      git.getRemotes(true, (err, all) => {
        if (err) {
          console.log(err);
        } else {
          let origin = all.find((i) => i.name === "origin");
          if (origin) {
            // try to match against a http or ssh url
            let data = origin.refs.push.match(/.+@.+\..+:(.+)\/(.+)\.git/) ||
                       origin.refs.push.match(/https?:\/\/.+\..+\/(.+)\/(.+)\.git/)
            if (data) {
              // form the url
              let url = `http://waltzapp.co/${data[1]}/${data[2]}`;
              console.log(url);
              if (!args.print) { open(url); }
            } else {
              console.log("Cannot do a regex match against", origin.refs.push);
            }
          } else {
            console.log("Cannot find a remote named origin to get a username and repositiory name from.");
          }
        }
      });
      break;

    default:
      if (args.help || args.h) {
        showHelp();
      } else if (args.version || args.v) {
        showVersion();
      } else {
        showNoCommand();
      }
      break;
  }
}
run(action, args);

function touchDotTimecard() {
  fs.mkdirpSync(`${process.env.HOME}/.timecard/templates`);
}

// given a timecard, calculate the total amount of time spent on the project.
function totalDuration(data, cbForEach) {
  return data.card.reduce((acc, day) => {
    return acc + day.times.reduce((acc, time) => {
      cbForEach && cbForEach(day, time);
      if (!day.disabled) {
        return acc + card.getDurationFor(day, time);
      } else {
        return acc;
      }
    }, 0);
  }, 0) / 1000 || 0;
}

// there was an error, somewhere
// catch it, and log it out.
function onError(error) {
  if (process.env.NODE_ENV !== "test") {
    console.log(chalk.red(`Oh no! There was an error!\n${error.toString()}`));
  } else {
    throw error;
  }
}

function showVersion() {
  console.log(`Waltz CLI version ${chalk.green(pkg.version)}`);
}

function showNoCommand() {
  card.echoLogo();
  showVersion();
  console.log(`No such command. Please run ${chalk.magenta("waltz --help")} for help.`);
}

function showHelp() {
  card.echoLogo();
  showVersion();
  console.log(`
Record your project development time.

Set up waltz with \`waltz init\`, then \`waltz in\` or \`waltz out\` to
record start or end time, respectively. Alternatively, use \`waltz watch\` to
manage recording your time automatically. To see a list of all times, run
\`waltz ls\`, and to generate a fancy-looking invoice, use \`waltz report\`.

Commands

  ---
  ${chalk.magenta("waltz init")}
  ---
  Create a new timecard in the current directory.
    \`--commit\` will fashion and make a new commit for this timecard.

  ---
  ${chalk.magenta("waltz in")}
  ---
  Clock in, starting a new time. This is typically run at the start of a work period.

  ---
  ${chalk.magenta("waltz out")}
  ---
  Clock out the current time. This is typically run at the end of a work period. If run with no unending times, it will error.

  ---
  ${chalk.magenta("waltz ls")} (aliased to \`waltz print\` and \`waltz list\`)
  ---
  List all times that are currently in the timecard.

  ---
  ${chalk.magenta("waltz paycheck")} (aliased to \`waltz markpaid\`)
  ---
  Marks all current times as paid.  After one is paid, run this command to reset the invoice total back to "zero", and start the next billing period. Times generated after this is run will still be unpaid, however.

  ---
  ${chalk.magenta("waltz report")} (aliased to \`waltz invoice\`)
  ---
  Generate an invoice and save in the current directory as \`report.html\`, by default.

    \`--print\` Print to stdout instead of writing to file.
    \`--file my_report.html\` Redirect the report to a different file.

  ---
  ${chalk.magenta("waltz watch")}
  ---
  This works by monitoring filesystem changes. When a file is updated, Waltz will record your start time, and after an expiry period, Waltz will record your end time. Because accuracy isn't guaranteed, if you need by-the-second times, use waltz in and waltz out.

    \`--expires 10\` Amount of minutes required of inactivity to
                 be considered "waltzed out". (default = 10 minutes)
    \`-- watchdir some/dir\` The root folder to watch for file changes
    \`-- verbose, -v\` Log all of the changes that waltz registers
    \`--quiet, -q\` Don't print anything to stdout (errors will still be logged)
    \`--ignore [Ii]gnore_me\` An ignore regex for files that shouldn't trigger a clock in or out.
                          Defaults to \`([\/\\]\.|node_modules|\.timecard\.json|\.git)\`

  ---
  ${chalk.magenta("waltz web")}
  ---
  Open the Waltz online app in your default browser.
    \`--print\` will print the url instead of opening the url in the browser.

  ---
  ${chalk.magenta("waltz commit")}
  ---
  Make a commit, using git, containing any changes made to the current timecard.
  Behind the scenes, this is equivelent to \`git add .timecard.json && git commit -m "Change work time via waltz"\`
    \`--message\` or \`-m\` Change the commit message

  ---
  ${chalk.magenta("Other")}
  ---
  -h, --help              Show this help message
  -v, --version           Show the current waltz cli version
  `.replace(/(`.*?`)/g, (match) => {
    // highlight all of the commands that are in backticks (remove them, and
    // change the color)
    return chalk.cyan(match.slice(1).slice(0, -1));
  }));
}

module.exports = {
  run,
};
<|endoftext|>


<commit_before>#!/usr/bin/env node

var cmd = require('commander')
, fs = require('fs')
, inquirer = require('inquirer')
, _package = require('../package.json')
, request = require('request')

var digio = (function () {

  var exec = function (options, callback) {
    options.url = 'https://api.digitalocean.com/v2/' + options.action
    options.headers = {
      json: true,
      Authorization: 'Bearer ' + options.token
    }

    delete options.action
    delete options.token

    request(options, function (err, res, body) {
      if (err) return callback(err)
      if (res.statusCode === 401) return callback(JSON.parse(body))

      callback(null, body)
    })
  }

  var list_droplets = function (token) {
    var options = {
      action: 'droplets',
      token: token
    }

    exec(options, function (err, res) {
      if (err) {
        console.log('Something went terribly wrong!')
        return
      }

      var json = JSON.parse(res)

      console.log('\nDroplets: ' + json.meta.total)

      json.droplets.forEach(function (droplet) {
        var result = '- ' + droplet.name + ' ('+droplet.id+')\n'
        console.log(result)
      })
    })
  }

  return {
    init: function () {
      cmd
        .version('Digio v' + _package.version)
        .option('droplets [token]', 'List all droplets', list_droplets)

      if (process.argv.length < 3) cmd.help()
      else cmd.parse(process.argv)
    }
  }

})()

digio.init()
<commit_msg>Fix Usage: undefined bug<commit_after>#!/usr/bin/env node

var cmd = require('commander')
, fs = require('fs')
, inquirer = require('inquirer')
, _package = require('../package.json')
, request = require('request')

var digio = (function () {

  var exec = function (options, callback) {
    options.url = 'https://api.digitalocean.com/v2/' + options.action
    options.headers = {
      json: true,
      Authorization: 'Bearer ' + options.token
    }

    delete options.action
    delete options.token

    request(options, function (err, res, body) {
      if (err) return callback(err)
      if (res.statusCode === 401) return callback(JSON.parse(body))

      callback(null, body)
    })
  }

  var list_droplets = function (token) {
    var options = {
      action: 'droplets',
      token: token
    }

    exec(options, function (err, res) {
      if (err) {
        console.log('Something went terribly wrong!')
        return
      }

      var json = JSON.parse(res)

      console.log('\nDroplets: ' + json.meta.total)

      json.droplets.forEach(function (droplet) {
        var result = '- ' + droplet.name + ' ('+droplet.id+')\n'
        console.log(result)
      })
    })
  }

  return {
    init: function () {
      cmd
        .version('Digio v' + _package.version)
        .option('droplets [token]', 'List all droplets', list_droplets)
        .parse(process.argv)

      if (process.argv.length < 3) cmd.help()
    }
  }

})()

digio.init()
<|endoftext|>


<commit_before>'use strict';

const path = require('path');

module.exports = function(env, callback) {
  class Sitemap extends env.plugins.Page {
    getFilename() {
      return 'sitemap.xml';
    }

    getEntries(contents) {
      return env.helpers.contents.list(contents).filter((content) => (
        content instanceof env.plugins.MarkdownPage &&
        !content.metadata.noindex
      ));
    }

    getView() {
      // jshint maxparams: 5
      return (env, locals, contents, templates, callback) => {
        const filepath = {
          'full': path.join(__dirname, 'templates', 'sitemap.jade'),
          'relative': path.join('templates', 'sitemap.jade'),
        };

        env.plugins.JadeTemplate.fromFile(filepath, (error, template) => {
          let context = {
            'entries': this.getEntries(contents),
            'page': this,
          };

          env.utils.extend(context, locals);

          template.render(context, callback);
        });
      };
    }
  }

  env.registerGenerator('sitemap', (contents, callback) => {
    callback(null, {'sitemap.xml': new Sitemap()});
  });

  callback();
};
<commit_msg>Clean up index file<commit_after>'use strict';

const path = require('path');

module.exports = function(env, callback) {
  class Sitemap extends env.plugins.Page {
    getFilename() {
      return 'sitemap.xml';
    }

    getView() {
      // jshint maxparams: 5
      return (env, locals, contents, templates, callback) => {
        const filepath = {
          'full': path.join(__dirname, 'templates', 'sitemap.jade'),
          'relative': path.join('templates', 'sitemap.jade'),
        };

        env.plugins.JadeTemplate.fromFile(filepath, (error, template) => {
          let context = {
            'entries': env.helpers.contents.list(contents).filter((content) => (
              content instanceof env.plugins.MarkdownPage &&
              !content.metadata.noindex
            )),
            'page': this,
          };

          env.utils.extend(context, locals);

          template.render(context, callback);
        });
      };
    }
  }

  env.registerGenerator('sitemap', (contents, callback) => {
    callback(null, {'sitemap.xml': new Sitemap()});
  });

  callback();
};
<|endoftext|>


<commit_before>/*
 * jitsu.js: Top-level include for the jitsu module.
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var path = require('path'),
    colors = require('colors'),
    flatiron = require('flatiron');

var jitsu = module.exports = flatiron.app;

//
// Setup `jitsu` to use `pkginfo` to expose version
//
require('pkginfo')(module, 'name', 'version');

//
// Configure jitsu to use `flatiron.plugins.cli`
//
jitsu.use(flatiron.plugins.cli, {
  version: true,
  usage: require('./jitsu/usage'),
  source: path.join(__dirname, 'jitsu', 'commands'),
  argv: {
    version: {
      alias: 'v',
      description: 'print jitsu version and exit',
      string: true
    },
    localconf: {
      description: 'search for .jitsuconf file in ./ and then parent directories',
      string: true
    },
    jitsuconf: {
      alias: 'j',
      description: 'specify file to load configuration from',
      string: true
    },
    noanalyze: {
      description: 'skip require-analyzer: do not attempt to dynamically detect dependencies',
      boolean: true
    },
    colors: {
      description: '--no-colors will disable output coloring',
      default: true,
      boolean: true
    },
    release: {
      alias: 'r',
      description: 'specify release version number or semantic increment (build, patch, minor, major)',
      string: true
    },
    raw: {
      description: 'jitsu will only output line-delimited raw JSON ( useful for piping )',
      boolean: true
    }
  }
});

jitsu.options.log = {
  console: {
    raw: jitsu.argv.raw
  }
};

//
// Setup config, users, command aliases and prompt settings
//
jitsu.prompt.properties = flatiron.common.mixin(
  jitsu.prompt.properties,
  require('./jitsu/properties')
);
jitsu.prompt.override   = jitsu.argv;
require('./jitsu/config');
require('./jitsu/alias');
require('./jitsu/commands');

//
// Setup other jitsu settings.
//
jitsu.started  = false;
jitsu.common   = require('./jitsu/common');
jitsu.package  = require('./jitsu/package');
jitsu.logFile  = new (require('./jitsu/common/logfile').LogFile)(path.join(process.env.HOME, '.jitsulog'));

//
// Hoist `jitsu.api` from `nodejitsu-api` module.
//
jitsu.api               = {};
jitsu.api.Client        = require('nodejitsu-api').Client;
jitsu.api.Apps          = require('nodejitsu-api').Apps;
jitsu.api.Databases     = require('nodejitsu-api').Databases;
jitsu.api.Logs          = require('nodejitsu-api').Logs;
jitsu.api.Snapshots     = require('nodejitsu-api').Snapshots;
jitsu.api.Users         = require('nodejitsu-api').Users;

//
// ### function welcome ()
// Print welcome message.
//
jitsu.welcome = function () {
  jitsu.log.info('Welcome to ' + 'Nodejitsu'.grey);
  jitsu.log.info('It worked if it ends with ' + 'Nodejitsu'.grey + ' ok'.green.bold);
};

//
// ### function start (command, callback)
// #### @command {string} Command to execute once started
// #### @callback {function} Continuation to pass control to when complete.
// Starts the jitsu CLI and runs the specified command.
//
jitsu.start = function (callback) {
  //
  // Check for --no-colors/--colors option, without hitting the config file
  // yet
  //
  var useColors = (typeof jitsu.argv.colors == 'undefined' || jitsu.argv.colors);

  useColors || (colors.mode = "none");

  jitsu.common.checkVersion(function (err) {
    if (err) {
      return callback();
    }

    jitsu.init(function (err) {
      if (err) {
        jitsu.welcome();
        callback(err);
        return jitsu.showError.apply(null, [command[0]].concat(arguments));
      }

      //
      // --no-colors option turns off output coloring, and so does setting
      // colors: false in ~/.jitsuconf (see
      // https://github.com/nodejitsu/jitsu/issues/101 )
      //
      if ( !jitsu.config.get('colors') || !useColors ) {
        colors.mode = "none";
        jitsu.log.get('default').stripColors = true;
        jitsu.log.get('default').transports.console.colorize = false;
      }

      jitsu.welcome();

      var username = jitsu.config.get('username');
      if (!username && jitsu.config.get('requiresAuth').indexOf(jitsu.argv._[0]) !== -1) {
        return jitsu.commands.users.login(function (err) {
          if (err) {
            callback(err);
            return jitsu.showError.apply(jitsu, [command[0]].concat(arguments));
          }

          var username = jitsu.config.get('username');
          jitsu.log.info('Successfully configured user ' + username.magenta);
          return jitsu.exec(jitsu.argv._, callback);
        });
      }

      return jitsu.exec(jitsu.argv._, callback);
    });
  });
};

//
// ### function exec (command, callback)
// #### @command {string} Command to execute
// #### @callback {function} Continuation to pass control to when complete.
// Runs the specified command in the jitsu CLI.
//
jitsu.exec = function (command, callback) {
  function execCommand (err) {
    if (err) {
      return callback(err);
    }

    //
    // Remark: This is a temporary fix for aliasing init=>install,
    // was having a few issues with the alias command on the install resource
    //
    if (command[0] === 'init') {
      command[0] = 'install';
    }

    // Alias db to databases
    if (command[0] === 'db') {
      command[0] = 'databases';
    }

    // Allow `jitsu logs` as a shortcut for `jitsu logs app`
    if (command[0] === 'logs' && command.length === 1) {
      command[1] = 'app';
    }

    jitsu.log.info('Executing command ' + command.join(' ').magenta);
    jitsu.router.dispatch('on', command.join(' '), jitsu.log, function (err, shallow) {
      if (err) {
        callback(err);
        return jitsu.showError(command.join(' '), err, shallow);
      }

      //
      // TODO (indexzero): Something here
      //
      callback();
    });
  }

  return !jitsu.started ? jitsu.setup(execCommand) : execCommand();
};

//
// ### function setup (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Sets up the instances of the Resource clients for jitsu.
// there is no io here, yet this function is ASYNC.
//
jitsu.setup = function (callback) {
  if (jitsu.started === true) {
    return callback();
  }

  ['Users', 'Apps', 'Snapshots', 'Databases', 'Logs'].forEach(function (key) {
    var k = key.toLowerCase();
    jitsu[k] = new jitsu.api[key](jitsu.config);
    jitsu[k].on('debug::request',  debug);
    jitsu[k].on('debug::response', debug);
    function debug (data) {
      if (jitsu.argv.debug || jitsu.config.get('debug')) {
        //
        // TODO: replace with custom logger method
        //
        // console.log(data);
      }
    };
  });

  jitsu.started = true;
  callback();
};

//
// ### function showError (command, err, shallow, skip)
// #### @command {string} Command which has errored.
// #### @err {Error} Error received for the command.
// #### @shallow {boolean} Value indicating if a deep stack should be displayed
// #### @skip {boolean} Value indicating if this error should be forcibly suppressed.
// Displays the `err` to the user for the `command` supplied.
//
jitsu.showError = function (command, err, shallow, skip) {
  var username,
      stack;

  if (err.statusCode === '403') {
    username = jitsu.config.get('username');
    if (username) {
      jitsu.log.error('Unable to authenticate as: ' + username.magenta);
    }

    jitsu.log.error('403 ' + err.result.error);
  }
  else if (err.statusCode === '503') {
    if (err.result && err.result.message) {
      jitsu.log.error(err.result.message);
    }
    else {
      jitsu.log.error('The Nodejitsu cloud is currently at capacity.  Please try again later.');
    }
  }
  else if (!skip) {
    jitsu.log.error('Error running command ' + command.magenta);

    if (!jitsu.config.get('nolog')) {
      jitsu.logFile.log(err);
    }

    if (err.message) {
      jitsu.log.error(err.message);
    }

    if (err.result) {
      if (err.result.error) {
        jitsu.log.error(err.result.error);
      }

      if (err.result.result && err.result.result.error) {
        if (err.result.result.error.stderr || err.result.result.error.stdout) {
          jitsu.log.error('');
          jitsu.log.error('There was an error while attempting to start your application.');
          jitsu.log.error(err.result.result.error.message);
          if (err.result.result.error.blame) {
            jitsu.log.error(err.result.result.error.blame.message);
            jitsu.log.error('');
            jitsu.log.error('This type of error is usually a ' + err.result.result.error.blame.type + ' error.');
          }

          jitsu.log.error('Error output from your application:');
          jitsu.log.error('');
          if (err.result.result.error.stdout) {
            err.result.result.error.stdout.split('\n').forEach(function (line) {
              jitsu.log.error(line);
            });
          }

          if (err.result.result.error.stderr) {
            err.result.result.error.stderr.split('\n').forEach(function (line) {
              jitsu.log.error(line);
            });
          }
        }
        else if (err.result.result.error.stack && jitsu.config.get('debug')) {
          jitsu.log.error('There was an error while attempting to deploy your application.');
          jitsu.log.error('');
          jitsu.log.error(err.result.result.error.message);

          if (err.result.result.error.blame) {
            jitsu.log.error(err.result.result.error.blame.message);
            jitsu.log.error('');
            jitsu.log.error('This type of error is usually a ' + err.result.result.error.blame.type + ' error.');
          }

          jitsu.log.error('Error output from Haibu:');
          jitsu.log.error('');
          stack = err.result.result.error.result || err.result.result.error.stack;
          stack.split('\n').forEach(function (line) {
            jitsu.log.error(line);
          });
        }
      }
      else if (err.result.stack) {
        jitsu.log.warn('Error returned from Nodejitsu');
        err.result.stack.split('\n').forEach(function (line) {
          jitsu.log.error(line);
        });
      }
    }
    else {
      if (err.stack && !shallow) {
        err.stack.split('\n').forEach(function (trace) {
          jitsu.log.error(trace);
        });
      }
    }
  }

  jitsu.log.info('Nodejitsu '.grey + 'not ok'.red.bold);
};
<commit_msg>[api] short-circuit whoami command from cli-users. whoami should not output anything but username<commit_after>/*
 * jitsu.js: Top-level include for the jitsu module.
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var path = require('path'),
    colors = require('colors'),
    flatiron = require('flatiron');

var jitsu = module.exports = flatiron.app;

//
// Setup `jitsu` to use `pkginfo` to expose version
//
require('pkginfo')(module, 'name', 'version');

//
// Configure jitsu to use `flatiron.plugins.cli`
//
jitsu.use(flatiron.plugins.cli, {
  version: true,
  usage: require('./jitsu/usage'),
  source: path.join(__dirname, 'jitsu', 'commands'),
  argv: {
    version: {
      alias: 'v',
      description: 'print jitsu version and exit',
      string: true
    },
    localconf: {
      description: 'search for .jitsuconf file in ./ and then parent directories',
      string: true
    },
    jitsuconf: {
      alias: 'j',
      description: 'specify file to load configuration from',
      string: true
    },
    noanalyze: {
      description: 'skip require-analyzer: do not attempt to dynamically detect dependencies',
      boolean: true
    },
    colors: {
      description: '--no-colors will disable output coloring',
      default: true,
      boolean: true
    },
    release: {
      alias: 'r',
      description: 'specify release version number or semantic increment (build, patch, minor, major)',
      string: true
    },
    raw: {
      description: 'jitsu will only output line-delimited raw JSON ( useful for piping )',
      boolean: true
    }
  }
});

jitsu.options.log = {
  console: {
    raw: jitsu.argv.raw
  }
};

//
// Setup config, users, command aliases and prompt settings
//
jitsu.prompt.properties = flatiron.common.mixin(
  jitsu.prompt.properties,
  require('./jitsu/properties')
);
jitsu.prompt.override   = jitsu.argv;
require('./jitsu/config');
require('./jitsu/alias');
require('./jitsu/commands');

//
// Setup other jitsu settings.
//
jitsu.started  = false;
jitsu.common   = require('./jitsu/common');
jitsu.package  = require('./jitsu/package');
jitsu.logFile  = new (require('./jitsu/common/logfile').LogFile)(path.join(process.env.HOME, '.jitsulog'));

//
// Hoist `jitsu.api` from `nodejitsu-api` module.
//
jitsu.api               = {};
jitsu.api.Client        = require('nodejitsu-api').Client;
jitsu.api.Apps          = require('nodejitsu-api').Apps;
jitsu.api.Databases     = require('nodejitsu-api').Databases;
jitsu.api.Logs          = require('nodejitsu-api').Logs;
jitsu.api.Snapshots     = require('nodejitsu-api').Snapshots;
jitsu.api.Users         = require('nodejitsu-api').Users;

//
// ### function welcome ()
// Print welcome message.
//
jitsu.welcome = function () {
  jitsu.log.info('Welcome to ' + 'Nodejitsu'.grey);
  jitsu.log.info('It worked if it ends with ' + 'Nodejitsu'.grey + ' ok'.green.bold);
};

//
// ### function start (command, callback)
// #### @command {string} Command to execute once started
// #### @callback {function} Continuation to pass control to when complete.
// Starts the jitsu CLI and runs the specified command.
//
jitsu.start = function (callback) {
  //
  // Check for --no-colors/--colors option, without hitting the config file
  // yet
  //
  var useColors = (typeof jitsu.argv.colors == 'undefined' || jitsu.argv.colors);

  useColors || (colors.mode = "none");

  //
  // whoami command should not output anything but username
  //
  if (jitsu.argv._[0] === "whoami") {
    console.log(jitsu.config.get('username'));
    return;
  }

  jitsu.common.checkVersion(function (err) {
    if (err) {
      return callback();
    }

    jitsu.init(function (err) {
      if (err) {
        jitsu.welcome();
        callback(err);
        return jitsu.showError.apply(null, [command[0]].concat(arguments));
      }

      //
      // --no-colors option turns off output coloring, and so does setting
      // colors: false in ~/.jitsuconf (see
      // https://github.com/nodejitsu/jitsu/issues/101 )
      //
      if ( !jitsu.config.get('colors') || !useColors ) {
        colors.mode = "none";
        jitsu.log.get('default').stripColors = true;
        jitsu.log.get('default').transports.console.colorize = false;
      }

      jitsu.welcome();

      var username = jitsu.config.get('username');
      if (!username && jitsu.config.get('requiresAuth').indexOf(jitsu.argv._[0]) !== -1) {
        return jitsu.commands.users.login(function (err) {
          if (err) {
            callback(err);
            return jitsu.showError.apply(jitsu, [command[0]].concat(arguments));
          }

          var username = jitsu.config.get('username');
          jitsu.log.info('Successfully configured user ' + username.magenta);
          return jitsu.exec(jitsu.argv._, callback);
        });
      }
      return jitsu.exec(jitsu.argv._, callback);
    });
  });
};

//
// ### function exec (command, callback)
// #### @command {string} Command to execute
// #### @callback {function} Continuation to pass control to when complete.
// Runs the specified command in the jitsu CLI.
//
jitsu.exec = function (command, callback) {
  function execCommand (err) {
    if (err) {
      return callback(err);
    }

    //
    // Remark: This is a temporary fix for aliasing init=>install,
    // was having a few issues with the alias command on the install resource
    //
    if (command[0] === 'init') {
      command[0] = 'install';
    }

    // Alias db to databases
    if (command[0] === 'db') {
      command[0] = 'databases';
    }

    // Allow `jitsu logs` as a shortcut for `jitsu logs app`
    if (command[0] === 'logs' && command.length === 1) {
      command[1] = 'app';
    }

    jitsu.log.info('Executing command ' + command.join(' ').magenta);
    jitsu.router.dispatch('on', command.join(' '), jitsu.log, function (err, shallow) {
      if (err) {
        callback(err);
        return jitsu.showError(command.join(' '), err, shallow);
      }

      //
      // TODO (indexzero): Something here
      //
      callback();
    });
  }

  return !jitsu.started ? jitsu.setup(execCommand) : execCommand();
};

//
// ### function setup (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Sets up the instances of the Resource clients for jitsu.
// there is no io here, yet this function is ASYNC.
//
jitsu.setup = function (callback) {
  if (jitsu.started === true) {
    return callback();
  }

  ['Users', 'Apps', 'Snapshots', 'Databases', 'Logs'].forEach(function (key) {
    var k = key.toLowerCase();
    jitsu[k] = new jitsu.api[key](jitsu.config);
    jitsu[k].on('debug::request',  debug);
    jitsu[k].on('debug::response', debug);
    function debug (data) {
      if (jitsu.argv.debug || jitsu.config.get('debug')) {
        //
        // TODO: replace with custom logger method
        //
        // console.log(data);
      }
    };
  });

  jitsu.started = true;
  callback();
};

//
// ### function showError (command, err, shallow, skip)
// #### @command {string} Command which has errored.
// #### @err {Error} Error received for the command.
// #### @shallow {boolean} Value indicating if a deep stack should be displayed
// #### @skip {boolean} Value indicating if this error should be forcibly suppressed.
// Displays the `err` to the user for the `command` supplied.
//
jitsu.showError = function (command, err, shallow, skip) {
  var username,
      stack;

  if (err.statusCode === '403') {
    username = jitsu.config.get('username');
    if (username) {
      jitsu.log.error('Unable to authenticate as: ' + username.magenta);
    }

    jitsu.log.error('403 ' + err.result.error);
  }
  else if (err.statusCode === '503') {
    if (err.result && err.result.message) {
      jitsu.log.error(err.result.message);
    }
    else {
      jitsu.log.error('The Nodejitsu cloud is currently at capacity.  Please try again later.');
    }
  }
  else if (!skip) {
    jitsu.log.error('Error running command ' + command.magenta);

    if (!jitsu.config.get('nolog')) {
      jitsu.logFile.log(err);
    }

    if (err.message) {
      jitsu.log.error(err.message);
    }

    if (err.result) {
      if (err.result.error) {
        jitsu.log.error(err.result.error);
      }

      if (err.result.result && err.result.result.error) {
        if (err.result.result.error.stderr || err.result.result.error.stdout) {
          jitsu.log.error('');
          jitsu.log.error('There was an error while attempting to start your application.');
          jitsu.log.error(err.result.result.error.message);
          if (err.result.result.error.blame) {
            jitsu.log.error(err.result.result.error.blame.message);
            jitsu.log.error('');
            jitsu.log.error('This type of error is usually a ' + err.result.result.error.blame.type + ' error.');
          }

          jitsu.log.error('Error output from your application:');
          jitsu.log.error('');
          if (err.result.result.error.stdout) {
            err.result.result.error.stdout.split('\n').forEach(function (line) {
              jitsu.log.error(line);
            });
          }

          if (err.result.result.error.stderr) {
            err.result.result.error.stderr.split('\n').forEach(function (line) {
              jitsu.log.error(line);
            });
          }
        }
        else if (err.result.result.error.stack && jitsu.config.get('debug')) {
          jitsu.log.error('There was an error while attempting to deploy your application.');
          jitsu.log.error('');
          jitsu.log.error(err.result.result.error.message);

          if (err.result.result.error.blame) {
            jitsu.log.error(err.result.result.error.blame.message);
            jitsu.log.error('');
            jitsu.log.error('This type of error is usually a ' + err.result.result.error.blame.type + ' error.');
          }

          jitsu.log.error('Error output from Haibu:');
          jitsu.log.error('');
          stack = err.result.result.error.result || err.result.result.error.stack;
          stack.split('\n').forEach(function (line) {
            jitsu.log.error(line);
          });
        }
      }
      else if (err.result.stack) {
        jitsu.log.warn('Error returned from Nodejitsu');
        err.result.stack.split('\n').forEach(function (line) {
          jitsu.log.error(line);
        });
      }
    }
    else {
      if (err.stack && !shallow) {
        err.stack.split('\n').forEach(function (trace) {
          jitsu.log.error(trace);
        });
      }
    }
  }

  jitsu.log.info('Nodejitsu '.grey + 'not ok'.red.bold);
};
<|endoftext|>


<commit_before>let list = {

    split(string, separators, last) {
        let array   = [];
        let current = '';
        let split   = false;

        let func    = 0;
        let quote   = false;
        let escape  = false;

        for ( let i = 0; i < string.length; i++ ) {
            let letter = string[i];

            if ( quote ) {
                if ( escape ) {
                    escape = false;
                } else if ( letter === '\\' ) {
                    escape = true;
                } else if ( letter === quote ) {
                    quote = false;
                }
            } else if ( letter === '"' || letter === '\'' ) {
                quote = letter;
            } else if ( letter === '(' ) {
                func += 1;
            } else if ( letter === ')' ) {
                if ( func > 0 ) func -= 1;
            } else if ( func === 0 ) {
                if ( separators.indexOf(letter) !== -1 ) split = true;
            }

            if ( split ) {
                if ( current !== '' ) array.push(current.trim());
                current = '';
                split   = false;
            } else {
                current += letter;
            }
        }

        if ( last || current !== '' ) array.push(current.trim());
        return array;
    },

    space(string) {
        let spaces = [' ', '\n', '\t'];
        return list.split(string, spaces);
    },

    comma(string) {
        let comma = ',';
        return list.split(string, [comma], true);
    }

};

export default list;
<commit_msg>Add JSDoc to list module<commit_after>/**
 * Contains helpers for safely splitting lists of CSS values,
 * preserving parentheses and quotes.
 *
 * @example
 * const list = postcss.list;
 *
 * @namespace list
 */
let list = {

    split(string, separators, last) {
        let array   = [];
        let current = '';
        let split   = false;

        let func    = 0;
        let quote   = false;
        let escape  = false;

        for ( let i = 0; i < string.length; i++ ) {
            let letter = string[i];

            if ( quote ) {
                if ( escape ) {
                    escape = false;
                } else if ( letter === '\\' ) {
                    escape = true;
                } else if ( letter === quote ) {
                    quote = false;
                }
            } else if ( letter === '"' || letter === '\'' ) {
                quote = letter;
            } else if ( letter === '(' ) {
                func += 1;
            } else if ( letter === ')' ) {
                if ( func > 0 ) func -= 1;
            } else if ( func === 0 ) {
                if ( separators.indexOf(letter) !== -1 ) split = true;
            }

            if ( split ) {
                if ( current !== '' ) array.push(current.trim());
                current = '';
                split   = false;
            } else {
                current += letter;
            }
        }

        if ( last || current !== '' ) array.push(current.trim());
        return array;
    },

    /**
     * Safely splits space-separated values (such as those for `background`,
     * `border-radius`, and other shorthand properties).
     *
     * @param {string} string - space-separated values
     *
     * @return {string[]} splitted values
     *
     * @example
     * postcss.list.space('1px calc(10% + 1px)') //=> ['1px', 'calc(10% + 1px)']
     */
    space(string) {
        let spaces = [' ', '\n', '\t'];
        return list.split(string, spaces);
    },

    /**
     * Safely splits comma-separated values (such as those for `transition-*`
     * and `background` properties).
     *
     * @param {string} string - comma-separated values
     *
     * @return {string[]} splitted values
     *
     * @example
     * postcss.list.comma('black, linear-gradient(white, black)')
     * //=> ['black', 'linear-gradient(white, black)']
     */
    comma(string) {
        let comma = ',';
        return list.split(string, [comma], true);
    }

};

export default list;
<|endoftext|>


<commit_before>var Log = require('./logger'),
  logger = new Log(global.logLevel),
  exec = require('child_process').execFile,
  fs = require('fs'),
  https = require('https'),
  windows = ((process.platform.match(/win32/) || process.platform.match(/win64/)) !== null),
  localBinary = __dirname + '/BrowserStackLocal' + (windows ? '.exe' : ''),
  utils = require('./utils'),
  config = require('./config');

var Tunnel = function Tunnel(key, port, uniqueIdentifier, callback) {
  var that = {};

  function tunnelLauncher() {
    var tunnelOptions = getTunnelOptions(key, uniqueIdentifier);

    if (typeof callback !== 'function') {
      callback = function(){};
    }

    logger.debug('[%s] Launching tunnel', new Date());

    var subProcess = exec(localBinary, tunnelOptions, function(error, stdout, stderr) {
      logger.debug(stderr);
      logger.debug(error);
      if (stdout.indexOf('Error') >= 0 || error) {
        logger.debug('[%s] Tunnel launching failed', new Date());
        logger.debug(stdout);
        process.exit('SIGINT');
      }
    });

    var data = '';
    var running = false;
    var runMatcher = 'You can now access your local server(s)';

    setTimeout(function() {
      if (!running) {
        utils.alertBrowserStack('Tunnel launch timeout', 'Stdout:\n' + data);
      }
    }, 30 * 1000);

    subProcess.stdout.on('data', function(_data) {
      if (running) {
        return;
      }

      data += _data;

      if (data.indexOf(runMatcher) >= 0) {
        running = true;
        logger.debug('[%s] Tunnel launched', new Date());
        setTimeout(function(){
          callback();
        }, 2000);
      }
    });

    that.process = subProcess;
  }

  function getTunnelOptions(key, uniqueIdentifier) {
    var options = [key];

    if (config.debug) {
      options.push('-v');
    }

    if (!uniqueIdentifier) {
      options.push('-force');
      options.push('-onlyAutomate');
    } else {
      options.push('-localIdentifier');
      options.push(uniqueIdentifier);
    }

    var proxy = config.proxy;

    if (proxy) {
      options.push('-proxyHost ' + proxy.host);
      options.push('-proxyPort ' + proxy.port);

      if (proxy.username && proxy.password) {
        options.push('-proxyUser ' + proxy.username);
        options.push('-proxyPass ' + proxy.password);
      }
    }

    return options;
  }

  fs.exists(localBinary, function(exists) {
    if (exists) {
      tunnelLauncher();
      return;
    }
    logger.debug('Downloading BrowserStack Local to "%s"', localBinary);

    var file = fs.createWriteStream(localBinary);
    https.get('https://s3.amazonaws.com/browserStack/browserstack-local/BrowserStackLocal' + (windows ? '.exe' : '-' + process.platform + '-' + process.arch),
    function(response) {
      response.pipe(file);

      response.on('end', function() {
        fs.chmodSync(localBinary, 0700);
        setTimeout(function() {
          tunnelLauncher();
        }, 100);
      }).on('error', function(e) {
        logger.info('Got error while downloading binary: ' + e.message);
        process.exit('SIGINT');
      });
    });
  });

  return that;
};

exports.Tunnel = Tunnel;
<commit_msg>Verify downloaded binary before attempting to start tunnel<commit_after>var Log = require('./logger'),
  logger = new Log(global.logLevel),
  exec = require('child_process').execFile,
  fs = require('fs'),
  https = require('https'),
  windows = ((process.platform.match(/win32/) || process.platform.match(/win64/)) !== null),
  localBinary = __dirname + '/BrowserStackLocal' + (windows ? '.exe' : ''),
  utils = require('./utils'),
  config = require('./config');

var Tunnel = function Tunnel(key, port, uniqueIdentifier, callback) {
  var that = {};

  function tunnelLauncher() {
    var tunnelOptions = getTunnelOptions(key, uniqueIdentifier);

    if (typeof callback !== 'function') {
      callback = function(){};
    }

    logger.debug('[%s] Launching tunnel', new Date());

    var subProcess = exec(localBinary, tunnelOptions, function(error, stdout, stderr) {
      logger.debug(stderr);
      logger.debug(error);
      if (stdout.indexOf('Error') >= 0 || error) {
        logger.debug('[%s] Tunnel launching failed', new Date());
        logger.debug(stdout);
        process.exit('SIGINT');
      }
    });

    var data = '';
    var running = false;
    var runMatcher = 'You can now access your local server(s)';

    setTimeout(function() {
      if (!running) {
        utils.alertBrowserStack('Tunnel launch timeout', 'Stdout:\n' + data);
      }
    }, 30 * 1000);

    subProcess.stdout.on('data', function(_data) {
      if (running) {
        return;
      }

      data += _data;

      if (data.indexOf(runMatcher) >= 0) {
        running = true;
        logger.debug('[%s] Tunnel launched', new Date());
        setTimeout(function(){
          callback();
        }, 2000);
      }
    });

    that.process = subProcess;
  }

  function getTunnelOptions(key, uniqueIdentifier) {
    var options = [key];

    if (config.debug) {
      options.push('-v');
    }

    if (!uniqueIdentifier) {
      options.push('-force');
      options.push('-onlyAutomate');
    } else {
      options.push('-localIdentifier');
      options.push(uniqueIdentifier);
    }

    var proxy = config.proxy;

    if (proxy) {
      options.push('-proxyHost ' + proxy.host);
      options.push('-proxyPort ' + proxy.port);

      if (proxy.username && proxy.password) {
        options.push('-proxyUser ' + proxy.username);
        options.push('-proxyPass ' + proxy.password);
      }
    }

    return options;
  }

  function runTunnelCmd(tunnelOptions, subProcessTimeout, processOutputHook, callback) {
    var isRunning, subProcess, timeoutHandle;

    var callbackOnce = function (err, result) {
      clearTimeout(timeoutHandle);
      if (subProcess && isRunning) {
        try {
          process.kill(subProcess.pid, 'SIGKILL');
          subProcess = null;
        } catch (e) {
          logger.debug('[%s] failed to kill process:', new Date(), e);
        }
      }

      callback && callback(err, result);
      callback = null;
    };

    isRunning = true;

    try {
      subProcess = exec(localBinary, tunnelOptions, function (error, stdout) {
        isRunning = false;

        if (error) {
          callbackOnce(new Error('failed to get process output: ' + error));
        } else if (stdout) {
          processOutputHook(stdout, callbackOnce);
        }
      });

      subProcess.stdout.on('data', function (data) {
        processOutputHook(data, callbackOnce);
      });
    } catch (e) {
      // Handles EACCESS and other errors when binary file exists,
      // but doesn't have necessary permissions (among other issues)
      callbackOnce(new Error('failed to get process output: ' + e));
    }

    if (subProcessTimeout > 0) {
      timeoutHandle = setTimeout(function () {
        callbackOnce(new Error('failed to get process output: command timeout'));
      }, subProcessTimeout);
    }
  }

  function getTunnelBinaryVersion(callback) {
    var subProcessTimeout = 3000;

    runTunnelCmd([ '-version' ], subProcessTimeout, function (data, done) {
      var matches = /version\s+(\d+(\.\d+)*)/.exec(data);
      var version = (matches && matches.length > 2) && matches[1];
      logger.debug('[%s] Tunnel binary: found version', new Date(), version);

      done(isFinite(version) ? null : new Error('failed to get binary version'), parseFloat(version));
    }, callback);
  }

  function verifyTunnelBinary(callback) {
    logger.debug('[%s] Verifying tunnel binary', new Date());

    fs.exists(localBinary, function (exists) {
      if (!exists) {
        logger.debug('[%s] Verifying tunnel binary: file does not exist', new Date());
        callback(false);
      } else {
        getTunnelBinaryVersion(function (err, version) {
          callback(!err && isFinite(version));
        });
      }
    });
  }

  verifyTunnelBinary(function (exists) {
    if (exists) {
      tunnelLauncher();
      return;
    }
    logger.debug('Downloading BrowserStack Local to "%s"', localBinary);

    var file = fs.createWriteStream(localBinary);
    https.get('https://s3.amazonaws.com/browserStack/browserstack-local/BrowserStackLocal' + (windows ? '.exe' : '-' + process.platform + '-' + process.arch),
    function(response) {
      response.pipe(file);

      response.on('end', function() {
        fs.chmodSync(localBinary, 0700);
        setTimeout(function() {
          tunnelLauncher();
        }, 100);
      }).on('error', function(e) {
        logger.info('Got error while downloading binary: ' + e.message);
        process.exit('SIGINT');
      });
    });
  });

  return that;
};

exports.Tunnel = Tunnel;
<|endoftext|>


<commit_before>"use strict";

const _ = require("lodash");
const Bluebird = require("bluebird");
const inquirer = require("inquirer");
const zlib = Bluebird.promisifyAll(require("zlib"));
const AWS = require("aws-sdk");
const cheerio = require("cheerio");
const uuid = require("uuid");
const puppeteer = require('puppeteer');
const querystring = require('querystring');
const debug = require("debug")('aws-azure-login');
const CLIError = require("./CLIError");
const awsConfig = require("./awsConfig");

const WIDTH = 425;
const HEIGHT = 550;
const DELAY_ON_UNRECOGNIZED_PAGE = 1000;
const MAX_UNRECOGNIZED_PAGE_DELAY = 30 * 1000;

const sts = new AWS.STS();

/**
 * To proxy the input/output of the Azure login page, it's easiest to run a loop that
 * monitors the state of the page and then perform the corresponding CLI behavior.
 * The states have a name that is used for the debug messages, a selector that is used
 * with puppeteer's page.$(selector) to determine if the state is active, and a handler
 * that is called if the state is active.
 */
const states = [
    {
        name: "username input",
        selector: `input[name="loginfmt"]:not(.moveOffScreen)`,
        async handler(page, _selected, defaultUsername) {
            const error = await page.$(".alert-error");
            if (error) {
                debug("Found error message. Displaying");
                const errorMessage = await page.evaluate(err => err.textContent, error);
                console.log(errorMessage);
            }

            debug("Prompting user for username");
            const { username } = await inquirer.prompt([{
                name: "username",
                message: "Username:",
                default: defaultUsername
            }]);

            debug("Focusing on username input");
            await page.focus(`input[name="loginfmt"]`);

            debug("Clearing input");
            for (let i = 0; i < 100; i++) {
                await page.keyboard.press("Backspace");
            }

            debug("Typing username");
            await page.keyboard.type(username);

            debug("Submitting form");
            await page.click("input[type=submit]");

            await Bluebird.delay(500);

            debug("Waiting for submission to finish");
            await Promise.race([
                page.waitForSelector(`input[name=loginfmt].has-error,input[name=loginfmt].moveOffScreen`, { timeout: 60000 }),
                (async () => {
                    await Bluebird.delay(1000);
                    await page.waitForSelector(`input[name=loginfmt]`, { hidden: true, timeout: 60000 });
                })()
            ]);
        }
    },
    {
        name: "password input",
        selector: `input[name="Password"]:not(.moveOffScreen)`,
        async handler(page) {
            const error = await page.$(".alert-error");
            if (error) {
                debug("Found error message. Displaying");
                const errorMessage = await page.evaluate(err => err.textContent, error);
                console.log(errorMessage);
            }

            debug("Prompting user for password");
            const { password } = await inquirer.prompt([{
                name: "password",
                message: "Password:",
                type: "password"
            }]);

            debug("Focusing on password input");
            await page.focus(`input[name="Password"]`);

            debug("Typing password");
            await page.keyboard.type(password);

            debug("Submitting form");
            await page.click("span[class=submit]");

            debug("Waiting for a delay");
            await Bluebird.delay(500);
        }
    },
    {
        name: 'TFA instructions',
        selector: `#idDiv_SAOTCAS_Description`,
        async handler(page, selected) {
            const descriptionMessage = await page.evaluate(description => description.textContent, selected);
            console.log(descriptionMessage);

            debug("Waiting for response");
            await page.waitForSelector(`#idDiv_SAOTCAS_Description`, { hidden: true, timeout: 60000 });
        }
    },
    {
        name: 'TFA failed',
        selector: `#idDiv_SAASDS_Description,#idDiv_SAASTO_Description`,
        async handler(page, selected) {
            const descriptionMessage = await page.evaluate(description => description.textContent, selected);
            throw new CLIError(descriptionMessage);
        }
    },
    {
        name: 'TFA code input',
        selector: "input[name=otc]:not(.moveOffScreen)",
        async handler(page) {
            const error = await page.$(".alert-error");
            if (error) {
                debug("Found error message. Displaying");
                const errorMessage = await page.evaluate(err => err.textContent, error);
                console.log(errorMessage);
            } else {
                const description = await page.$("#idDiv_SAOTCC_Description");
                const descriptionMessage = await page.evaluate(description => description.textContent, description);
                console.log(descriptionMessage);
            }

            const { verificationCode } = await inquirer.prompt([{
                name: "verificationCode",
                message: "Verification Code:"
            }]);

            debug("Focusing on verification code input");
            await page.focus(`input[name="otc"]`);

            debug("Clearing input");
            for (let i = 0; i < 100; i++) {
                await page.keyboard.press("Backspace");
            }

            debug("Typing verification code");
            await page.keyboard.type(verificationCode);

            debug("Submitting form");
            await page.click("input[type=submit]");

            debug("Waiting for submission to finish");
            await Promise.race([
                page.waitForSelector(`input[name=otc].has-error,input[name=otc].moveOffScreen`, { timeout: 60000 }),
                (async () => {
                    await Bluebird.delay(1000);
                    await page.waitForSelector(`input[name=otc]`, { hidden: true, timeout: 60000 });
                })()
            ]);
        }
    },
    {
        name: "Remember me",
        selector: `#KmsiDescription`,
        async handler(page) {
            debug("Clicking don't remember button");
            await page.click("#idBtn_Back");

            debug("Waiting for a delay");
            await Bluebird.delay(500);
        }
    },
    {
        name: "Service exception",
        selector: "#service_exception_message",
        async handler(page, selected) {
            const descriptionMessage = await page.evaluate(description => description.textContent, selected);
            throw new CLIError(descriptionMessage);
        }
    }
];

module.exports = {
    async loginAsync(profileName, mode, disableSandbox) {
        let headless, cliProxy;
        if (mode === 'cli') {
            headless = true;
            cliProxy = true;
        } else if (mode === 'gui') {
            headless = false;
            cliProxy = false;
        } else if (mode === 'debug') {
            headless = false;
            cliProxy = true;
        } else {
            throw new CLIError('Invalid mode');
        }

        const profile = await this._loadProfileAsync(profileName);
        const loginUrl = await this._createLoginUrlAsync(profile.azure_app_id_uri, profile.azure_tenant_id);
        const samlResponse = await this._performLoginAsync(loginUrl, headless, disableSandbox, cliProxy, profile.azure_default_username);
        const roles = this._parseRolesFromSamlResponse(samlResponse);
        const role = await this._askUserForRoleAsync(roles, profile.azure_default_role_arn);
        await this._assumeRoleAsync(profileName, samlResponse, role);
    },

    /**
     * Load the profile.
     * @param {string} profileName - The name of the profile.
     * @returns {Promise.<{}>} The profile.
     * @private
     */
    async _loadProfileAsync(profileName) {
        const profile = await awsConfig.getProfileConfigAsync(profileName);
        if (!profile) throw new CLIError(`Unknown profile '${profileName}'. You must configure it first with --configure.`);
        if (!profile.azure_tenant_id || !profile.azure_app_id_uri) throw new CLIError(`Profile '${profileName}' is not configured properly.`);

        console.log(`Logging in with profile '${profileName}'...`);
        return profile;
    },

    /**
     * Create the Azure login SAML URL.
     * @param {string} appIdUri - The app ID URI
     * @param {string} tenantId - The Azure tenant ID
     * @returns {string} The login URL
     * @private
     */
    async _createLoginUrlAsync(appIdUri, tenantId) {
        debug("Generating UUID for SAML request");
        const id = uuid.v4();
        const samlRequest = `
        <samlp:AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:metadata" ID="id${id}" Version="2.0" IssueInstant="${new Date().toISOString()}" IsPassive="false" AssertionConsumerServiceURL="https://signin.aws.amazon.com/saml" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
            <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">${appIdUri}</Issuer>
            <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"></samlp:NameIDPolicy>
        </samlp:AuthnRequest>
        `;
        debug("Generated SAML request", samlRequest);

        debug("Deflating SAML");
        const samlBuffer = await zlib.deflateRawAsync(samlRequest);

        debug("Encoding SAML in base64");
        const samlBase64 = samlBuffer.toString('base64');

        const url = `https://login.microsoftonline.com/${tenantId}/saml2?SAMLRequest=${encodeURIComponent(samlBase64)}`;
        debug("Created login URL", url);

        return url;
    },

    /**
     * Perform the login using Chrome.
     * @param {string} url - The login URL
     * @param {boolean} headless - True to hide the GUI, false to show it.
     * @param {boolean} disableSandbox - True to disable the Puppeteer sandbox.
     * @param {boolean} cliProxy - True to proxy input/output through the CLI, false to leave it in the GUI
     * @param {string} [defaultUsername] - The default username
     * @returns {Promise.<string>} The SAML response.
     * @private
     */
    async _performLoginAsync(url, headless, disableSandbox, cliProxy, defaultUsername) {
        debug("Loading login page in Chrome");
        let browser;
        try {
            const args = headless ? [] : [`--app=${url}`, `--window-size=${WIDTH},${HEIGHT}`];
            if (disableSandbox) args.push('--no-sandbox');

            const browser = await puppeteer.launch({
                headless,
                args
            });

            const pages = await browser.pages();
            const page = pages[0];
            await page.setViewport({ width: WIDTH - 15, height: HEIGHT - 35 });

            // Prevent redirection to AWS
            let samlResponseData;
            const samlResponsePromise = new Promise(resolve => {
                page.on('request', req => {
                    const url = req.url();
                    debug(`Request: ${url}`);
                    if (url === 'https://signin.aws.amazon.com/saml') {
                        resolve();
                        samlResponseData = req.postData();
                        req.respond({
                            status: 200,
                            contentType: 'text/plain',
                            body: ''
                        });
                        browser.close();
                    } else {
                        req.continue();
                    }
                });
            });

            debug("Enabling request interception");
            await page.setRequestInterception(true);

            if (headless) {
                debug("Going to login page");
                await page.goto(url, { waitUntil: 'networkidle0' });
            } else {
                debug("Waiting for login page to load");
                await page.waitForNavigation({ waitUntil: 'networkidle0' });
            }

            if (cliProxy) {
                let totalUnrecognizedDelay = 0;
                while (true) {
                    if (samlResponseData) break;

                    let foundState = false;
                    for (let i = 0; i < states.length; i++) {
                        const state = states[i];

                        let selected;
                        try {
                            selected = await page.$(state.selector);
                        } catch (err) {
                            // An error can be thrown if the page isn't in a good state.
                            // If one occurs, try again after another loop.
                            break;
                        }

                        if (selected) {
                            foundState = true;
                            debug(`Found state: ${state.name}`);

                            await Promise.race([
                                samlResponsePromise,
                                state.handler(page, selected, defaultUsername)
                            ]);

                            debug(`Finished state: ${state.name}`);

                            break;
                        }
                    }

                    if (foundState) {
                        totalUnrecognizedDelay = 0;
                    } else {
                        debug("State not recognized!");
                        if (totalUnrecognizedDelay > MAX_UNRECOGNIZED_PAGE_DELAY) {
                            const path = 'aws-azure-login-unrecognized-state.png';
                            await page.screenshot({ path });
                            throw new CLIError(`Unable to recognize page state! A screenshot has been dumped to ${path}. If this problem persists, try running with --mode=gui or --mode=debug`);
                        }

                        totalUnrecognizedDelay += DELAY_ON_UNRECOGNIZED_PAGE;
                        await Bluebird.delay(DELAY_ON_UNRECOGNIZED_PAGE);
                    }
                }
            } else {
                console.log("Please complete the login in the opened window");
                await samlResponsePromise;
            }

            const samlResponse = querystring.parse(samlResponseData).SAMLResponse;
            debug("Found SAML response", samlResponse);

            return samlResponse;
        } finally {
            if (browser) browser.close();
        }
    },

    /**
     * Parse AWS roles out of the SAML response
     * @param {string} assertion - The SAML assertion
     * @returns {Array.<{roleArn: string, principalArn: string}>} The roles
     * @private
     */
    _parseRolesFromSamlResponse(assertion) {
        debug("Converting assertion from base64 to ASCII");
        const samlText = new Buffer(assertion, 'base64').toString("ascii");
        debug("Converted", samlText);

        debug("Parsing SAML XML");
        const saml = cheerio.load(samlText, { xmlMode: true });

        debug("Looking for role SAML attribute");
        const roles = saml("Attribute[Name='https://aws.amazon.com/SAML/Attributes/Role']>AttributeValue").map(function () {
            const roleAndPrincipal = saml(this).text();
            const parts = roleAndPrincipal.split(",");

            // Role / Principal claims may be in either order
            const [roleIdx, principalIdx] = parts[0].indexOf(":role/") >= 0 ? [0, 1] : [1, 0];
            const roleArn = parts[roleIdx].trim();
            const principalArn = parts[principalIdx].trim();
            return { roleArn, principalArn };
        }).get();
        debug("Found roles", roles);
        return roles;
    },

    /**
     * Ask the user for the role they want to use.
     * @param {Array.<{roleArn: string, principalArn: string}>} roles - The roles to pick from
     * @param {string} [defaultRoleArn] - The default role ARN
     * @returns {Promise.<string>} The selected role
     * @private
     */
    async _askUserForRoleAsync(roles, defaultRoleArn) {
        let role;
        if (roles.length === 0) {
            throw new CLIError("No roles found in SAML response.");
        } else if (roles.length === 1) {
            role = roles[0];
        } else {
            debug("Asking user to choose role");
            const answers = await inquirer.prompt([{
                name: "role",
                message: "Role:",
                type: "list",
                choices: _.map(roles, "roleArn"),
                default: defaultRoleArn
            }]);

            role = _.find(roles, ["roleArn", answers.role]);
        }

        return role;
    },

    /**
     * Assume the role.
     * @param {string} profileName - The profile name
     * @param {string} assertion - The SAML assertion
     * @param {string} role - The role to assume
     * @returns {Promise} A promise
     * @private
     */
    async _assumeRoleAsync(profileName, assertion, role) {
        console.log(`Assuming role ${role.roleArn}`);
        const res = await sts.assumeRoleWithSAML({
            PrincipalArn: role.principalArn,
            RoleArn: role.roleArn,
            SAMLAssertion: assertion
        }).promise();

        await awsConfig.setProfileCredentialsAsync(profileName, {
            aws_access_key_id: res.Credentials.AccessKeyId,
            aws_secret_access_key: res.Credentials.SecretAccessKey,
            aws_session_token: res.Credentials.SessionToken
        });
    }
};
<commit_msg>update selector to handle both old and new login page<commit_after>"use strict";

const _ = require("lodash");
const Bluebird = require("bluebird");
const inquirer = require("inquirer");
const zlib = Bluebird.promisifyAll(require("zlib"));
const AWS = require("aws-sdk");
const cheerio = require("cheerio");
const uuid = require("uuid");
const puppeteer = require('puppeteer');
const querystring = require('querystring');
const debug = require("debug")('aws-azure-login');
const CLIError = require("./CLIError");
const awsConfig = require("./awsConfig");

const WIDTH = 425;
const HEIGHT = 550;
const DELAY_ON_UNRECOGNIZED_PAGE = 1000;
const MAX_UNRECOGNIZED_PAGE_DELAY = 30 * 1000;

const sts = new AWS.STS();

/**
 * To proxy the input/output of the Azure login page, it's easiest to run a loop that
 * monitors the state of the page and then perform the corresponding CLI behavior.
 * The states have a name that is used for the debug messages, a selector that is used
 * with puppeteer's page.$(selector) to determine if the state is active, and a handler
 * that is called if the state is active.
 */
const states = [
    {
        name: "username input",
        selector: `input[name="loginfmt"]:not(.moveOffScreen)`,
        async handler(page, _selected, defaultUsername) {
            const error = await page.$(".alert-error");
            if (error) {
                debug("Found error message. Displaying");
                const errorMessage = await page.evaluate(err => err.textContent, error);
                console.log(errorMessage);
            }

            debug("Prompting user for username");
            const { username } = await inquirer.prompt([{
                name: "username",
                message: "Username:",
                default: defaultUsername
            }]);

            debug("Focusing on username input");
            await page.focus(`input[name="loginfmt"]`);

            debug("Clearing input");
            for (let i = 0; i < 100; i++) {
                await page.keyboard.press("Backspace");
            }

            debug("Typing username");
            await page.keyboard.type(username);

            debug("Submitting form");
            await page.click("input[type=submit]");

            await Bluebird.delay(500);

            debug("Waiting for submission to finish");
            await Promise.race([
                page.waitForSelector(`input[name=loginfmt].has-error,input[name=loginfmt].moveOffScreen`, { timeout: 60000 }),
                (async () => {
                    await Bluebird.delay(1000);
                    await page.waitForSelector(`input[name=loginfmt]`, { hidden: true, timeout: 60000 });
                })()
            ]);
        }
    },
    {
        name: "password input",
        selector: `input[name="Password"]:not(.moveOffScreen),input[name="passwd"]:not(.moveOffScreen)`,
        async handler(page) {
            const error = await page.$(".alert-error");
            if (error) {
                debug("Found error message. Displaying");
                const errorMessage = await page.evaluate(err => err.textContent, error);
                console.log(errorMessage);
            }

            debug("Prompting user for password");
            const { password } = await inquirer.prompt([{
                name: "password",
                message: "Password:",
                type: "password"
            }]);

            debug("Focusing on password input");
            await page.focus(`input[name="Password"],input[name="passwd"]`);

            debug("Typing password");
            await page.keyboard.type(password);

            debug("Submitting form");
            await page.click("span[class=submit],input[type=submit]");

            debug("Waiting for a delay");
            await Bluebird.delay(500);
        }
    },
    {
        name: 'TFA instructions',
        selector: `#idDiv_SAOTCAS_Description`,
        async handler(page, selected) {
            const descriptionMessage = await page.evaluate(description => description.textContent, selected);
            console.log(descriptionMessage);

            debug("Waiting for response");
            await page.waitForSelector(`#idDiv_SAOTCAS_Description`, { hidden: true, timeout: 60000 });
        }
    },
    {
        name: 'TFA failed',
        selector: `#idDiv_SAASDS_Description,#idDiv_SAASTO_Description`,
        async handler(page, selected) {
            const descriptionMessage = await page.evaluate(description => description.textContent, selected);
            throw new CLIError(descriptionMessage);
        }
    },
    {
        name: 'TFA code input',
        selector: "input[name=otc]:not(.moveOffScreen)",
        async handler(page) {
            const error = await page.$(".alert-error");
            if (error) {
                debug("Found error message. Displaying");
                const errorMessage = await page.evaluate(err => err.textContent, error);
                console.log(errorMessage);
            } else {
                const description = await page.$("#idDiv_SAOTCC_Description");
                const descriptionMessage = await page.evaluate(description => description.textContent, description);
                console.log(descriptionMessage);
            }

            const { verificationCode } = await inquirer.prompt([{
                name: "verificationCode",
                message: "Verification Code:"
            }]);

            debug("Focusing on verification code input");
            await page.focus(`input[name="otc"]`);

            debug("Clearing input");
            for (let i = 0; i < 100; i++) {
                await page.keyboard.press("Backspace");
            }

            debug("Typing verification code");
            await page.keyboard.type(verificationCode);

            debug("Submitting form");
            await page.click("input[type=submit]");

            debug("Waiting for submission to finish");
            await Promise.race([
                page.waitForSelector(`input[name=otc].has-error,input[name=otc].moveOffScreen`, { timeout: 60000 }),
                (async () => {
                    await Bluebird.delay(1000);
                    await page.waitForSelector(`input[name=otc]`, { hidden: true, timeout: 60000 });
                })()
            ]);
        }
    },
    {
        name: "Remember me",
        selector: `#KmsiDescription`,
        async handler(page) {
            debug("Clicking don't remember button");
            await page.click("#idBtn_Back");

            debug("Waiting for a delay");
            await Bluebird.delay(500);
        }
    },
    {
        name: "Service exception",
        selector: "#service_exception_message",
        async handler(page, selected) {
            const descriptionMessage = await page.evaluate(description => description.textContent, selected);
            throw new CLIError(descriptionMessage);
        }
    }
];

module.exports = {
    async loginAsync(profileName, mode, disableSandbox) {
        let headless, cliProxy;
        if (mode === 'cli') {
            headless = true;
            cliProxy = true;
        } else if (mode === 'gui') {
            headless = false;
            cliProxy = false;
        } else if (mode === 'debug') {
            headless = false;
            cliProxy = true;
        } else {
            throw new CLIError('Invalid mode');
        }

        const profile = await this._loadProfileAsync(profileName);
        const loginUrl = await this._createLoginUrlAsync(profile.azure_app_id_uri, profile.azure_tenant_id);
        const samlResponse = await this._performLoginAsync(loginUrl, headless, disableSandbox, cliProxy, profile.azure_default_username);
        const roles = this._parseRolesFromSamlResponse(samlResponse);
        const role = await this._askUserForRoleAsync(roles, profile.azure_default_role_arn);
        await this._assumeRoleAsync(profileName, samlResponse, role);
    },

    /**
     * Load the profile.
     * @param {string} profileName - The name of the profile.
     * @returns {Promise.<{}>} The profile.
     * @private
     */
    async _loadProfileAsync(profileName) {
        const profile = await awsConfig.getProfileConfigAsync(profileName);
        if (!profile) throw new CLIError(`Unknown profile '${profileName}'. You must configure it first with --configure.`);
        if (!profile.azure_tenant_id || !profile.azure_app_id_uri) throw new CLIError(`Profile '${profileName}' is not configured properly.`);

        console.log(`Logging in with profile '${profileName}'...`);
        return profile;
    },

    /**
     * Create the Azure login SAML URL.
     * @param {string} appIdUri - The app ID URI
     * @param {string} tenantId - The Azure tenant ID
     * @returns {string} The login URL
     * @private
     */
    async _createLoginUrlAsync(appIdUri, tenantId) {
        debug("Generating UUID for SAML request");
        const id = uuid.v4();
        const samlRequest = `
        <samlp:AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:metadata" ID="id${id}" Version="2.0" IssueInstant="${new Date().toISOString()}" IsPassive="false" AssertionConsumerServiceURL="https://signin.aws.amazon.com/saml" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
            <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">${appIdUri}</Issuer>
            <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"></samlp:NameIDPolicy>
        </samlp:AuthnRequest>
        `;
        debug("Generated SAML request", samlRequest);

        debug("Deflating SAML");
        const samlBuffer = await zlib.deflateRawAsync(samlRequest);

        debug("Encoding SAML in base64");
        const samlBase64 = samlBuffer.toString('base64');

        const url = `https://login.microsoftonline.com/${tenantId}/saml2?SAMLRequest=${encodeURIComponent(samlBase64)}`;
        debug("Created login URL", url);

        return url;
    },

    /**
     * Perform the login using Chrome.
     * @param {string} url - The login URL
     * @param {boolean} headless - True to hide the GUI, false to show it.
     * @param {boolean} disableSandbox - True to disable the Puppeteer sandbox.
     * @param {boolean} cliProxy - True to proxy input/output through the CLI, false to leave it in the GUI
     * @param {string} [defaultUsername] - The default username
     * @returns {Promise.<string>} The SAML response.
     * @private
     */
    async _performLoginAsync(url, headless, disableSandbox, cliProxy, defaultUsername) {
        debug("Loading login page in Chrome");
        let browser;
        try {
            const args = headless ? [] : [`--app=${url}`, `--window-size=${WIDTH},${HEIGHT}`];
            if (disableSandbox) args.push('--no-sandbox');

            const browser = await puppeteer.launch({
                headless,
                args
            });

            const pages = await browser.pages();
            const page = pages[0];
            await page.setViewport({ width: WIDTH - 15, height: HEIGHT - 35 });

            // Prevent redirection to AWS
            let samlResponseData;
            const samlResponsePromise = new Promise(resolve => {
                page.on('request', req => {
                    const url = req.url();
                    debug(`Request: ${url}`);
                    if (url === 'https://signin.aws.amazon.com/saml') {
                        resolve();
                        samlResponseData = req.postData();
                        req.respond({
                            status: 200,
                            contentType: 'text/plain',
                            body: ''
                        });
                        browser.close();
                    } else {
                        req.continue();
                    }
                });
            });

            debug("Enabling request interception");
            await page.setRequestInterception(true);

            if (headless) {
                debug("Going to login page");
                await page.goto(url, { waitUntil: 'networkidle0' });
            } else {
                debug("Waiting for login page to load");
                await page.waitForNavigation({ waitUntil: 'networkidle0' });
            }

            if (cliProxy) {
                let totalUnrecognizedDelay = 0;
                while (true) {
                    if (samlResponseData) break;

                    let foundState = false;
                    for (let i = 0; i < states.length; i++) {
                        const state = states[i];

                        let selected;
                        try {
                            selected = await page.$(state.selector);
                        } catch (err) {
                            // An error can be thrown if the page isn't in a good state.
                            // If one occurs, try again after another loop.
                            break;
                        }

                        if (selected) {
                            foundState = true;
                            debug(`Found state: ${state.name}`);

                            await Promise.race([
                                samlResponsePromise,
                                state.handler(page, selected, defaultUsername)
                            ]);

                            debug(`Finished state: ${state.name}`);

                            break;
                        }
                    }

                    if (foundState) {
                        totalUnrecognizedDelay = 0;
                    } else {
                        debug("State not recognized!");
                        if (totalUnrecognizedDelay > MAX_UNRECOGNIZED_PAGE_DELAY) {
                            const path = 'aws-azure-login-unrecognized-state.png';
                            await page.screenshot({ path });
                            throw new CLIError(`Unable to recognize page state! A screenshot has been dumped to ${path}. If this problem persists, try running with --mode=gui or --mode=debug`);
                        }

                        totalUnrecognizedDelay += DELAY_ON_UNRECOGNIZED_PAGE;
                        await Bluebird.delay(DELAY_ON_UNRECOGNIZED_PAGE);
                    }
                }
            } else {
                console.log("Please complete the login in the opened window");
                await samlResponsePromise;
            }

            const samlResponse = querystring.parse(samlResponseData).SAMLResponse;
            debug("Found SAML response", samlResponse);

            return samlResponse;
        } finally {
            if (browser) browser.close();
        }
    },

    /**
     * Parse AWS roles out of the SAML response
     * @param {string} assertion - The SAML assertion
     * @returns {Array.<{roleArn: string, principalArn: string}>} The roles
     * @private
     */
    _parseRolesFromSamlResponse(assertion) {
        debug("Converting assertion from base64 to ASCII");
        const samlText = new Buffer(assertion, 'base64').toString("ascii");
        debug("Converted", samlText);

        debug("Parsing SAML XML");
        const saml = cheerio.load(samlText, { xmlMode: true });

        debug("Looking for role SAML attribute");
        const roles = saml("Attribute[Name='https://aws.amazon.com/SAML/Attributes/Role']>AttributeValue").map(function () {
            const roleAndPrincipal = saml(this).text();
            const parts = roleAndPrincipal.split(",");

            // Role / Principal claims may be in either order
            const [roleIdx, principalIdx] = parts[0].indexOf(":role/") >= 0 ? [0, 1] : [1, 0];
            const roleArn = parts[roleIdx].trim();
            const principalArn = parts[principalIdx].trim();
            return { roleArn, principalArn };
        }).get();
        debug("Found roles", roles);
        return roles;
    },

    /**
     * Ask the user for the role they want to use.
     * @param {Array.<{roleArn: string, principalArn: string}>} roles - The roles to pick from
     * @param {string} [defaultRoleArn] - The default role ARN
     * @returns {Promise.<string>} The selected role
     * @private
     */
    async _askUserForRoleAsync(roles, defaultRoleArn) {
        let role;
        if (roles.length === 0) {
            throw new CLIError("No roles found in SAML response.");
        } else if (roles.length === 1) {
            role = roles[0];
        } else {
            debug("Asking user to choose role");
            const answers = await inquirer.prompt([{
                name: "role",
                message: "Role:",
                type: "list",
                choices: _.map(roles, "roleArn"),
                default: defaultRoleArn
            }]);

            role = _.find(roles, ["roleArn", answers.role]);
        }

        return role;
    },

    /**
     * Assume the role.
     * @param {string} profileName - The profile name
     * @param {string} assertion - The SAML assertion
     * @param {string} role - The role to assume
     * @returns {Promise} A promise
     * @private
     */
    async _assumeRoleAsync(profileName, assertion, role) {
        console.log(`Assuming role ${role.roleArn}`);
        const res = await sts.assumeRoleWithSAML({
            PrincipalArn: role.principalArn,
            RoleArn: role.roleArn,
            SAMLAssertion: assertion
        }).promise();

        await awsConfig.setProfileCredentialsAsync(profileName, {
            aws_access_key_id: res.Credentials.AccessKeyId,
            aws_secret_access_key: res.Credentials.SecretAccessKey,
            aws_session_token: res.Credentials.SessionToken
        });
    }
};
<|endoftext|>