1 | 'use strict'
|
2 |
|
3 | const Heroku = require('heroku-client')
|
4 | const cli = require('..')
|
5 | const auth = require('./auth')
|
6 | const vars = require('./vars')
|
7 | const Mutex = require('./mutex')
|
8 |
|
9 | function 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 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | if (res.statusCode === 403 && body.id === 'two_factor' && !preauths.requests.includes(this)) {
|
25 | let self = this
|
26 |
|
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 |
|
40 |
|
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 |
|
61 | function 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 |
|
68 | if (!this._handleFailure) {
|
69 | this._handleFailure = this.handleFailure
|
70 | this.handleFailure = twoFactor.bind(this)
|
71 | }
|
72 |
|
73 | cb()
|
74 | }
|
75 | }
|
76 |
|
77 | function 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 |
|
109 | let httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy
|
110 |
|
111 | function 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 |
|
120 | function relogin () {
|
121 | if (process.env.HEROKU_API_KEY) {
|
122 | cli.error(`API key is currently set by the HEROKU_API_KEY environment variable.
|
123 | Ensure 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 |
|
129 | function 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 |
|
144 | function reasonPrompt (context) {
|
145 | return cli.prompt('Reason')
|
146 | .then(function (reason) {
|
147 | context.reason = reason
|
148 | })
|
149 | }
|
150 |
|
151 | module.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 | }
|