UNPKG

7.94 kBJavaScriptView Raw
1'use strict'
2
3const cli = require('heroku-cli-util')
4const path = require('path');
5const child = require('child_process');
6const url = require('url');
7const co = require('co');
8const keypair = require('keypair');
9const forge = require('node-forge');
10const socks = require('@heroku/socksv5');
11const ssh = require('./ssh')
12const wait = require('co-wait')
13const Client = require('ssh2').Client;
14
15function * checkStatus(context, heroku, configVars) {
16 let dynos = yield heroku.request({path: `/apps/${context.app}/dynos`})
17
18 var execUrl = _execUrl(context, configVars)
19
20 return cli.got(`https://${execUrl.host}`, {
21 auth: execUrl.auth,
22 path: _execApiPath(configVars),
23 headers: _execHeaders(),
24 method: 'GET'
25 }).then(response => {
26
27 var reservations = JSON.parse(response.body);
28
29 cli.styledHeader(`Heroku Exec ${cli.color.app(context.app)}`)
30
31 if (reservations.length == 0) {
32 cli.error("Heroku Exec is not running!")
33 cli.error("Check dyno status with `heroku ps'")
34 } else {
35
36 var statuses = []
37
38 for (var i in reservations) {
39 var name = reservations[i]['dyno_name']
40 var dyno = dynos.find(d => d.name === name)
41
42 statuses.push({
43 dyno_name: cli.color.white.bold(name),
44 proxy_status: 'running',
45 dyno_status: !dyno ? cli.color.red('missing!') : (dyno.state === 'up' ? cli.color.green(dyno.state) : cli.color.yellow(dyno.state))
46 })
47 }
48 cli.table(statuses, {
49 columns: [
50 {key: 'dyno_name', label: 'Dyno'},
51 {key: 'proxy_status', label: 'Proxy Status'},
52 {key: 'dyno_status', label: 'Dyno Status'},
53 ]
54 });
55 }
56 }).catch(error => {
57 cli.error(error);
58 });;
59}
60
61function * initFeature(context, heroku, callback) {
62 var buildpackUrls = ["https://github.com/heroku/exec-buildpack", "urn:buildpack:heroku/exec"]
63 let promises = {
64 app: heroku.get(`/apps/${context.app}`),
65 feature: heroku.get(`/apps/${context.app}/features/runtime-heroku-exec`),
66 config: heroku.get(`/apps/${context.app}/config-vars`),
67 buildpacks: heroku.request({
68 path: `/apps/${context.app}/buildpack-installations`,
69 headers: {Range: ''}
70 })
71 }
72
73 let data = yield promises
74 let feature = data.feature
75 let configVars = data.config
76 let buildpacks = data.buildpacks
77
78 if (data.app['space'] != null) {
79 if (data.app['space']['shield'] === true) {
80 cli.error(`This feature is restricted for Shield Private Spaces`)
81 cli.exit(1);
82 } else if (buildpacks.length === 0) {
83 cli.error(`${context.app} has no Buildpack URL set. You must deploy your application first!`)
84 cli.exit(1);
85 } else if (!(_hasExecBuildpack(buildpacks, buildpackUrls))) {
86 yield _enableFeature(context, heroku)
87 cli.log(`Adding the Heroku Exec buildpack to ${context.app}`)
88 child.execSync(`heroku buildpacks:add -i 1 heroku/exec -a ${context.app}`)
89 cli.log('')
90 cli.log('Run the following commands to redeploy your app, then Heroku Exec will be ready to use:')
91 cli.log(cli.color.magenta(' git commit -m "Heroku Exec initialization" --allow-empty'))
92 cli.log(cli.color.magenta(' git push heroku master'))
93 cli.exit(0);
94 }
95 } else if (_hasExecBuildpack(buildpacks, buildpackUrls)) {
96 cli.warn(`The Heroku Exec buildpack is no longer required for this app,\n` +
97 `and may interfer with the 'heroku run' command. Please run the\n` +
98 `following command to remove it:\n ` +
99 cli.color.magenta('heroku buildpacks:remove https://github.com/heroku/exec-buildpack'))
100 }
101
102 var addonUrl = configVars['HEROKU_EXEC_URL']
103 if (addonUrl) {
104 cli.error("It looks like you're using the Heroku Exec addon, which is no longer required\n" +
105 "to use this feature. Please run the following command to remove the addon\n" +
106 "and then try using Heroku Exec again:\n" +
107 cli.color.magenta(' heroku addons:destroy heroku-exec'));
108 cli.exit();
109 } else if (!feature.enabled) {
110 cli.log(`Running this command for the first time requires a dyno restart.`)
111 let answer = yield cli.prompt('Do you want to continue? [y/n]', {});
112
113 if (answer.trim().toLowerCase() !== 'y') {
114 cli.exit();
115 }
116
117 yield _enableFeature(context, heroku)
118
119 yield cli.action(`Restarting dynos`, co(function * () {
120 yield wait(2000)
121 yield heroku.request({method: 'DELETE', path: `/apps/${context.app}/dynos`});
122 }))
123
124 let dynoName = _dyno(context)
125 let state = 'down'
126 yield cli.action(`Waiting for ${cli.color.cyan(dynoName)} to start`, co(function * () {
127 while (state != 'up') {
128 yield wait(3000)
129 let d = yield heroku.request({path: `/apps/${context.app}/dynos/${dynoName}`})
130 state = d.state
131 if (state === 'crashed') {
132 throw new Error(`The dyno crashed`)
133 }
134 }
135 }))
136 }
137
138 yield callback(configVars);
139}
140
141function updateClientKey(context, heroku, configVars, callback) {
142 return cli.action("Establishing credentials", {success: false}, co(function* () {
143 var key = keypair();
144 var privkeypem = key.private;
145 var publicKey = forge.pki.publicKeyFromPem(key.public);
146 var pubkeypem = forge.ssh.publicKeyToOpenSSH(publicKey, '');
147 cli.hush(pubkeypem)
148
149 var execUrl = _execUrl(context, configVars)
150 var dyno = _dyno(context)
151
152 return cli.got(`https://${execUrl.host}`, {
153 auth: execUrl.auth,
154 path: `${_execApiPath(configVars)}/${dyno}`,
155 method: 'PUT',
156 headers: _execHeaders(),
157 body: {client_key: pubkeypem}
158 }).then(function (response) {
159 cli.action.done('done')
160 callback(privkeypem, dyno, response);
161 }).catch(error => {
162 cli.action.done('error');
163 cli.hush(error);
164 cli.error('Could not connect to dyno!\nCheck if the dyno is running with `heroku ps\'')
165 });;
166 }))
167}
168
169
170function createSocksProxy(context, heroku, configVars, callback) {
171 return updateClientKey(context, heroku, configVars, function(key, dyno, response) {
172 cli.hush(response.body);
173 var json = JSON.parse(response.body);
174 var user = json['client_user']
175 var host = json['tunnel_host']
176 var port = 80
177 var dyno_ip = json['dyno_ip']
178
179 ssh.socksv5({ host: host, port: port, username: user, privateKey: key }, function(socks_port) {
180 if (callback) callback(dyno_ip, dyno, socks_port)
181 else cli.log(`Use ${cli.color.magenta('CTRL+C')} to stop the proxy`)
182 });
183 })
184}
185
186function _execApiPath(configVars) {
187 if (configVars['HEROKU_EXEC_URL']) {
188 return '/api/v1';
189 } else {
190 return '/api/v2'
191 }
192}
193
194function _execUrl(context, configVars) {
195 var urlString = configVars['HEROKU_EXEC_URL']
196 if (urlString) {
197 return url.parse(urlString);
198 } else {
199 if (process.env.HEROKU_EXEC_URL === undefined) {
200 urlString = "https://exec-manager.heroku.com/"
201 } else {
202 urlString = process.env.HEROKU_EXEC_URL
203 }
204 var execUrl = url.parse(urlString)
205 execUrl.auth = `${context.app}:${process.env.HEROKU_API_KEY || context.auth.password}`
206 return execUrl
207 }
208}
209
210function _dyno(context) {
211 return context.flags.dyno || 'web.1'
212}
213
214function _hasExecBuildpack(buildpacks, urls) {
215 for (let b of buildpacks) {
216 for (let u of urls) {
217 if (b['buildpack']['url'].indexOf(u) === 0) return true
218 }
219 }
220 return false
221}
222
223function _enableFeature(context, heroku) {
224 return cli.action('Initializing feature', co(function* () {
225 yield heroku.request({
226 method: 'PATCH',
227 path: `/apps/${context.app}/features/runtime-heroku-exec`,
228 body: {'enabled' : true}
229 });
230 }));
231}
232
233function _execHeaders() {
234 if (process.env.HEROKU_HEADERS) {
235 cli.hush(`using headers: ${process.env.HEROKU_HEADERS}`)
236 return JSON.parse(process.env.HEROKU_HEADERS)
237 } else {
238 return {}
239 }
240}
241
242module.exports = {
243 createSocksProxy,
244 checkStatus,
245 initFeature,
246 updateClientKey
247}