heroku-cli-util
Version:
Set of helpful CLI utilities
172 lines (153 loc) • 5.6 kB
JavaScript
const Heroku = require('heroku-client')
const cli = require('..')
const auth = require('./auth')
const vars = require('./vars')
const Mutex = require('./mutex')
function twoFactorWrapper (options, preauths, context) {
return function (res, buffer) {
let body
try {
body = this.parseBody(buffer)
} catch (e) {
this._handleFailure(res, buffer)
}
// safety check for if we have already seen this request for preauthing
// this prevents an infinite loop in case some preauth fails silently
// and we continue to get two_factor failures
// this might be better done with a timer in case a command takes too long
// and the preauthorization runs out, but that seemed unlikely
if (res.statusCode === 403 && body.id === 'two_factor' && !preauths.requests.includes(this)) {
let self = this
// default preauth to always happen unless explicitly disabled
if (options.preauth === false || !body.app) {
twoFactorPrompt(options, preauths, context)
.then(function (secondFactor) {
self.options.headers = Object.assign({}, self.options.headers, {'Heroku-Two-Factor-Code': secondFactor})
self.request()
})
.catch(function (err) {
self.reject(err)
})
} else {
preauths.requests.push(self)
// if multiple requests are run in parallel for the same app, we should
// only preauth for the first so save the fact we already preauthed
if (!preauths.promises[body.app.name]) {
preauths.promises[body.app.name] = twoFactorPrompt(options, preauths, context)
.then(function (secondFactor) {
return cli.preauth(body.app.name, heroku(context), secondFactor)
})
}
preauths.promises[body.app.name].then(function () {
self.request()
})
.catch(function (err) {
self.reject(err)
})
}
} else {
this._handleFailure(res, buffer)
}
}
}
function apiMiddleware (options, preauths, context) {
let twoFactor = twoFactorWrapper(options, preauths, context)
return function (response, cb) {
let warning = response.headers['x-heroku-warning'] || response.headers['warning-message']
if (warning) cli.action.warn(warning)
// override the _handleFailure for this request
if (!this._handleFailure) {
this._handleFailure = this.handleFailure
this.handleFailure = twoFactor.bind(this)
}
cb()
}
}
function heroku (context, options) {
let host = context.apiUrl || vars.apiUrl || 'https://api.heroku.com'
let preauths = {
promises: {},
requests: [],
twoFactorMutex: new Mutex()
}
let opts = {
userAgent: context.version,
debug: context.debug,
debugHeaders: context.debugHeaders,
token: context.auth ? context.auth.password : null,
host: host,
headers: {},
rejectUnauthorized: !(process.env.HEROKU_SSL_VERIFY === 'disable' || host.endsWith('herokudev.com')),
middleware: apiMiddleware(options, preauths, context)
}
if (process.env.HEROKU_HEADERS) {
Object.assign(opts.headers, JSON.parse(process.env.HEROKU_HEADERS))
}
if (context.secondFactor) {
Object.assign(opts.headers, {'Heroku-Two-Factor-Code': context.secondFactor})
}
if (context.reason) {
Object.assign(opts.headers, {'X-Heroku-Sudo-Reason': context.reason})
}
cli.heroku = new Heroku(opts)
return cli.heroku
}
let httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy
function setupHttpProxy () {
const url = require('url')
cli.hush(`proxy set to ${httpsProxy}`)
let proxy = url.parse(httpsProxy)
process.env.HEROKU_HTTP_PROXY_HOST = proxy.hostname
process.env.HEROKU_HTTP_PROXY_PORT = proxy.port
process.env.HEROKU_HTTP_PROXY_AUTH = proxy.auth
}
function relogin () {
if (process.env.HEROKU_LOGIN_REDIRECT === '1') return auth.login({save: true})
process.env.HEROKU_LOGIN_REDIRECT = '1'
require('child_process').execSync('heroku login', {stdio: 'inherit'})
return Promise.resolve()
}
function twoFactorPrompt (options, preauths, context) {
cli.yubikey.enable()
return preauths.twoFactorMutex.synchronize(function () {
return cli.prompt('Two-factor code', {mask: true})
.catch(function (err) {
cli.yubikey.disable()
throw err
})
.then(function (secondFactor) {
cli.yubikey.disable()
return secondFactor
})
})
}
function reasonPrompt (context) {
return cli.prompt('Reason')
.then(function (reason) {
context.reason = reason
})
}
module.exports = function command (options, fn) {
return function (context) {
if (typeof options === 'function') [fn, options] = [options, {}]
if (httpsProxy) setupHttpProxy()
cli.color.enabled = context.supportsColor
let handleErr = cli.errorHandler({debug: context.debug})
let run = function () {
context.auth = {password: auth.token()}
let p = fn(context, heroku(context, options))
if (!p.catch) return
return p.catch(function (err) {
if (err && err.body && err.body.id === 'unauthorized') {
cli.error(err.body.message || 'Unauthorized')
return relogin().then(run).catch(handleErr)
} else if (err && err.body && err.body.id === 'sudo_reason_required') {
cli.warn(err.body.message)
return reasonPrompt(context).then(run).catch(handleErr)
} else throw err
}).catch(handleErr)
}
return run()
}
}