UNPKG

5.59 kBJavaScriptView Raw
1'use strict'
2
3const Heroku = require('heroku-client')
4const cli = require('..')
5const auth = require('./auth')
6const vars = require('./vars')
7const Mutex = require('./mutex')
8
9function twoFactorWrapper (options, preauths, context) {
10 return function (res, buffer) {
11 let body
12 try {
13 body = this.parseBody(buffer)
14 } catch (e) {
15 this._handleFailure(res, buffer)
16 }
17
18 // safety check for if we have already seen this request for preauthing
19 // this prevents an infinite loop in case some preauth fails silently
20 // and we continue to get two_factor failures
21
22 // this might be better done with a timer in case a command takes too long
23 // and the preauthorization runs out, but that seemed unlikely
24 if (res.statusCode === 403 && body.id === 'two_factor' && !preauths.requests.includes(this)) {
25 let self = this
26 // default preauth to always happen unless explicitly disabled
27 if (options.preauth === false || !body.app) {
28 twoFactorPrompt(options, preauths, context)
29 .then(function (secondFactor) {
30 self.options.headers = Object.assign({}, self.options.headers, {'Heroku-Two-Factor-Code': secondFactor})
31 self.request()
32 })
33 .catch(function (err) {
34 self.reject(err)
35 })
36 } else {
37 preauths.requests.push(self)
38
39 // if multiple requests are run in parallel for the same app, we should
40 // only preauth for the first so save the fact we already preauthed
41 if (!preauths.promises[body.app.name]) {
42 preauths.promises[body.app.name] = twoFactorPrompt(options, preauths, context)
43 .then(function (secondFactor) {
44 return cli.preauth(body.app.name, heroku(context), secondFactor)
45 })
46 }
47
48 preauths.promises[body.app.name].then(function () {
49 self.request()
50 })
51 .catch(function (err) {
52 self.reject(err)
53 })
54 }
55 } else {
56 this._handleFailure(res, buffer)
57 }
58 }
59}
60
61function apiMiddleware (options, preauths, context) {
62 let twoFactor = twoFactorWrapper(options, preauths, context)
63 return function (response, cb) {
64 let warning = response.headers['x-heroku-warning'] || response.headers['warning-message']
65 if (warning) cli.action.warn(warning)
66
67 // override the _handleFailure for this request
68 if (!this._handleFailure) {
69 this._handleFailure = this.handleFailure
70 this.handleFailure = twoFactor.bind(this)
71 }
72
73 cb()
74 }
75}
76
77function heroku (context, options) {
78 let host = context.apiUrl || vars.apiUrl || 'https://api.heroku.com'
79
80 let preauths = {
81 promises: {},
82 requests: [],
83 twoFactorMutex: new Mutex()
84 }
85
86 let opts = {
87 userAgent: context.version,
88 debug: context.debug,
89 debugHeaders: context.debugHeaders,
90 token: context.auth ? context.auth.password : null,
91 host: host,
92 headers: {},
93 rejectUnauthorized: !(process.env.HEROKU_SSL_VERIFY === 'disable' || host.endsWith('herokudev.com')),
94 middleware: apiMiddleware(options, preauths, context)
95 }
96 if (process.env.HEROKU_HEADERS) {
97 Object.assign(opts.headers, JSON.parse(process.env.HEROKU_HEADERS))
98 }
99 if (context.secondFactor) {
100 Object.assign(opts.headers, {'Heroku-Two-Factor-Code': context.secondFactor})
101 }
102 if (context.reason) {
103 Object.assign(opts.headers, {'X-Heroku-Sudo-Reason': context.reason})
104 }
105 cli.heroku = new Heroku(opts)
106 return cli.heroku
107}
108
109let httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy
110
111function setupHttpProxy () {
112 const url = require('url')
113 cli.hush(`proxy set to ${httpsProxy}`)
114 let proxy = url.parse(httpsProxy)
115 process.env.HEROKU_HTTP_PROXY_HOST = proxy.hostname
116 process.env.HEROKU_HTTP_PROXY_PORT = proxy.port
117 process.env.HEROKU_HTTP_PROXY_AUTH = proxy.auth
118}
119
120function relogin () {
121 if (process.env.HEROKU_API_KEY) {
122 cli.error(`API key is currently set by the HEROKU_API_KEY environment variable.
123Ensure this is set to a correct value or unset it to use the netrc file.`)
124 process.exit(1)
125 }
126 return auth.login({save: true})
127}
128
129function twoFactorPrompt (options, preauths, context) {
130 cli.yubikey.enable()
131 return preauths.twoFactorMutex.synchronize(function () {
132 return cli.prompt('Two-factor code', {mask: true})
133 .catch(function (err) {
134 cli.yubikey.disable()
135 throw err
136 })
137 .then(function (secondFactor) {
138 cli.yubikey.disable()
139 return secondFactor
140 })
141 })
142}
143
144function reasonPrompt (context) {
145 return cli.prompt('Reason')
146 .then(function (reason) {
147 context.reason = reason
148 })
149}
150
151module.exports = function command (options, fn) {
152 return function (context) {
153 if (typeof options === 'function') [fn, options] = [options, {}]
154 if (httpsProxy) setupHttpProxy()
155 cli.color.enabled = context.supportsColor
156 let handleErr = cli.errorHandler({debug: context.debug})
157 let run = function () {
158 context.auth = {password: auth.token()}
159 let p = fn(context, heroku(context, options))
160 if (!p.catch) return
161 return p.catch(function (err) {
162 if (err && err.body && err.body.id === 'unauthorized') {
163 cli.error(err.body.message || 'Unauthorized')
164 return relogin().then(run).catch(handleErr)
165 } else if (err && err.body && err.body.id === 'sudo_reason_required') {
166 cli.warn(err.body.message)
167 return reasonPrompt(context).then(run).catch(handleErr)
168 } else throw err
169 }).catch(handleErr)
170 }
171 return run()
172 }
173}