1 | 'use strict'
|
2 |
|
3 | const child = require('child_process');
|
4 | const cli = require('heroku-cli-util');
|
5 | const co = require('co');
|
6 | const Client = require('ssh2').Client;
|
7 | const https = require('https')
|
8 | const url = require('url');
|
9 | const tty = require('tty')
|
10 | const stream = require('stream')
|
11 | const fs = require('fs')
|
12 | const socks = require('@heroku/socksv5');
|
13 | const progress = require('smooth-progress')
|
14 | const temp = require('temp')
|
15 |
|
16 | function 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 |
|
84 | function 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 |
|
119 | function 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 |
|
164 | function _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 |
|
170 | function _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 |
|
184 | function _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 |
|
209 | function 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 |
|
241 | function _buildCommand (args) {
|
242 | if (args.length === 1) {
|
243 |
|
244 |
|
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 |
|
257 | module.exports = {
|
258 | ssh,
|
259 | socksv5,
|
260 | connect,
|
261 | scp
|
262 | }
|