UNPKG

7.49 kBJavaScriptView Raw
1'use strict'
2
3const child = require('child_process');
4const cli = require('heroku-cli-util');
5const co = require('co');
6const Client = require('ssh2').Client;
7const https = require('https')
8const url = require('url');
9const tty = require('tty')
10const stream = require('stream')
11const fs = require('fs')
12const socks = require('@heroku/socksv5');
13const progress = require('smooth-progress')
14const temp = require('temp')
15
16function connect(context, addonHost, dynoUser, privateKey, callback) {
17 return new Promise((resolve, reject) => {
18 var conn = new Client();
19 cli.hush("[cli-ssh] created")
20 conn.on('ready', function() {
21 cli.hush("[cli-ssh] ready")
22 cli.action.done('up')
23 if (context.args.length > 0 && context.args != 'bash') {
24 let cmd = _buildCommand(context.args)
25 cli.hush(`[cli-ssh] command: ${cmd}`)
26 conn.exec(cmd, function(err, stream) {
27 cli.hush("[cli-ssh] exec")
28 if (err) {
29 cli.hush(`[cli-ssh] err: ${err}`)
30 throw err;
31 }
32 stream.on('close', function(code, signal) {
33 cli.hush("[cli-ssh] close")
34 conn.end();
35 resolve();
36 if (callback) callback();
37 })
38 .on('data', _readData(stream))
39 .on('error', reject);
40 process.once('SIGINT', () => conn.end())
41 });
42 } else {
43 cli.hush("[cli-ssh] bash")
44 conn.shell(function(err, stream) {
45 cli.hush("[cli-ssh] shell")
46 if (err) {
47 cli.hush(`[cli-ssh] err: ${err}`)
48 return _logConnectionError(err);
49 }
50 stream.on('close', function() {
51 cli.hush("[cli-ssh] close")
52 conn.end();
53 resolve();
54 })
55 .on('data', _readData(stream))
56 .on('error', function (err) {
57 cli.hush(err)
58 cli.error("There was a networking error! Please try connecting again.")
59 reject
60 })
61 process.once('SIGINT', () => conn.end())
62 });
63 }
64 }).on('error', function(err) {
65 cli.hush(err)
66 if (err.message === "Keepalive timeout") {
67 cli.error("Connection to the dyno timed out!")
68 } else {
69 cli.error("There was an error connecting to the dyno!")
70 }
71 reject
72 }).connect({
73 host: addonHost,
74 port: 80,
75 username: dynoUser,
76 privateKey: privateKey,
77 keepaliveInterval: 10000,
78 keepaliveCountMax: 3,
79 debug: cli.hush
80 });
81 });
82}
83
84function ssh(context, dynoUser, tunnelHost, privateKey) {
85 cli.hush("[cli-ssh] native")
86 return new Promise((resolve, reject) => {
87 temp.track();
88 temp.open('heroku-exec-key', function(err, info) {
89 if (!err) {
90 fs.writeSync(info.fd, privateKey);
91 fs.close(info.fd, function(err) {
92 fs.chmodSync(`${info.path}`, "0700")
93 let sshCommand = "ssh " +
94 "-o UserKnownHostsFile=/dev/null " +
95 "-o StrictHostKeyChecking=no " +
96 "-o ServerAliveInterval=10 " +
97 "-o ServerAliveCountMax=3 " +
98 "-p 80 " +
99 `-i ${info.path} ` +
100 `${dynoUser}@${tunnelHost} `
101
102 if (context.args.length > 0 && context.args != 'bash') {
103 sshCommand = `${sshCommand} ${_buildCommand(context.args)}`
104 }
105
106 try {
107 child.execSync(sshCommand, { stdio: ['inherit', 'inherit', 'ignore' ] }
108 )
109 } catch (e) {
110 if (e.stderr) cli.hush(e.stderr)
111 cli.hush(`[cli-ssh] exit: ${e.status}, ${e.message}`)
112 }
113 });
114 }
115 });
116 });
117}
118
119function scp(context, addonHost, dynoUser, privateKey, src, dest) {
120 return new Promise((resolve, reject) => {
121 var conn = new Client();
122 conn.on('ready', function() {
123 cli.action.done('up')
124 conn.sftp(function(err, sftp) {
125 if (err) {
126 return _logConnectionError(err);
127 }
128
129 var bar = false;
130 var progressCallback = function (totalTransferred, chunk, totalFile) {
131 if (!bar) {
132 bar = progress({
133 tmpl: 'Downloading... :bar :percent :eta',
134 width: 25,
135 total: totalFile
136 })
137 }
138 bar.tick(chunk, totalTransferred)
139 };
140
141 sftp.fastGet(src, dest, {
142 step: function (totalTransferred, chunk, totalFile) {
143 progressCallback(totalTransferred, chunk, totalFile);
144 }
145 }, function(error) {
146 if (error) {
147 cli.hush(error)
148 cli.error("ERROR: Could not transfer the file!");
149 cli.error("Make sure the filename is correct.");
150 }
151 conn.end();
152 resolve();
153 });
154 });
155 }).on('error', reject).connect({
156 host: addonHost,
157 port: 80,
158 username: dynoUser,
159 privateKey: privateKey
160 });
161 });
162}
163
164function _logConnectionError(err) {
165 cli.error("ERROR: Could not connect to the dyno!");
166 cli.error(`Check that the dyno is active by running ${cli.color.white.bold("heroku ps")}`);
167 return err;
168}
169
170function _readData (c) {
171 let firstLine = true
172 return function(data) {
173 if (firstLine) {
174 firstLine = false
175 _readStdin(c)
176 }
177 if (data) {
178 data = data.toString().replace(' \r', '\n')
179 process.stdout.write(data)
180 }
181 }
182}
183
184function _readStdin (c) {
185 let stdin = process.stdin
186 stdin.setEncoding('utf8')
187 if (stdin.unref) stdin.unref()
188 if (tty.isatty(0)) {
189 stdin.setRawMode(true)
190 stdin.pipe(c)
191 let sigints = []
192 stdin.on('data', function (c) {
193 if (c === '\u0003') sigints.push(new Date())
194 sigints = sigints.filter(d => d > new Date() - 1000)
195 if (sigints.length >= 4) {
196 cli.error('forcing dyno disconnect')
197 process.exit(1)
198 }
199 })
200 } else {
201 stdin.pipe(new stream.Transform({
202 objectMode: true,
203 transform: (chunk, _, next) => c.write(chunk, next),
204 flush: done => c.write('\x04', done)
205 }))
206 }
207}
208
209function socksv5(ssh_config, callback) {
210 var socksPort = 1080;
211 socks.createServer(function(info, accept, deny) {
212 var conn = new Client();
213 conn.on('ready', function() {
214 conn.forwardOut(info.srcAddr,
215 info.srcPort,
216 info.dstAddr,
217 info.dstPort,
218 function(err, stream) {
219 if (err) {
220 conn.end();
221 return deny();
222 }
223
224 var clientSocket;
225 if (clientSocket = accept(true)) {
226 stream.pipe(clientSocket).pipe(stream).on('close', function() {
227 conn.end();
228 });
229 } else
230 conn.end();
231 });
232 }).on('error', function(err) {
233 deny();
234 }).connect(ssh_config);
235 }).listen(socksPort, 'localhost', function() {
236 console.log(`SOCKSv5 proxy server started on port ${cli.color.white.bold(socksPort)}`);
237 if (callback) callback(socksPort);
238 }).useAuth(socks.auth.None());
239}
240
241function _buildCommand (args) {
242 if (args.length === 1) {
243 // do not add quotes around arguments if there is only one argument
244 // `heroku run "rake test"` should work like `heroku run rake test`
245 return args[0]
246 }
247 let cmd = ''
248 for (let arg of args) {
249 if (arg.indexOf(' ') !== -1 || arg.indexOf('"') !== -1) {
250 arg = '"' + arg.replace(/"/g, '\\"') + '"'
251 }
252 cmd = cmd + ' ' + arg
253 }
254 return cmd.trim()
255}
256
257module.exports = {
258 ssh,
259 socksv5,
260 connect,
261 scp
262}