Mercurial > 510Connectbot
comparison src/com/five_ten_sg/connectbot/transport/SSH.java @ 0:0ce5cc452d02
initial version
author | Carl Byington <carl@five-ten-sg.com> |
---|---|
date | Thu, 22 May 2014 10:41:19 -0700 |
parents | |
children | 3b760b39962a |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:0ce5cc452d02 |
---|---|
1 /* | |
2 * ConnectBot: simple, powerful, open-source SSH client for Android | |
3 * Copyright 2007 Kenny Root, Jeffrey Sharkey | |
4 * | |
5 * Licensed under the Apache License, Version 2.0 (the "License"); | |
6 * you may not use this file except in compliance with the License. | |
7 * You may obtain a copy of the License at | |
8 * | |
9 * http://www.apache.org/licenses/LICENSE-2.0 | |
10 * | |
11 * Unless required by applicable law or agreed to in writing, software | |
12 * distributed under the License is distributed on an "AS IS" BASIS, | |
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 * See the License for the specific language governing permissions and | |
15 * limitations under the License. | |
16 */ | |
17 | |
18 package com.five_ten_sg.connectbot.transport; | |
19 | |
20 import java.io.File; | |
21 import java.io.IOException; | |
22 import java.io.InputStream; | |
23 import java.io.OutputStream; | |
24 import java.net.InetAddress; | |
25 import java.net.InetSocketAddress; | |
26 import java.net.URL; | |
27 import java.net.MalformedURLException; | |
28 import java.security.KeyPair; | |
29 import java.security.NoSuchAlgorithmException; | |
30 import java.security.PrivateKey; | |
31 import java.security.PublicKey; | |
32 import java.security.interfaces.DSAPrivateKey; | |
33 import java.security.interfaces.DSAPublicKey; | |
34 import java.security.interfaces.RSAPrivateKey; | |
35 import java.security.interfaces.RSAPublicKey; | |
36 import java.security.spec.InvalidKeySpecException; | |
37 import java.util.Arrays; | |
38 import java.util.HashMap; | |
39 import java.util.LinkedList; | |
40 import java.util.List; | |
41 import java.util.Locale; | |
42 import java.util.Map; | |
43 import java.util.Map.Entry; | |
44 import java.util.regex.Matcher; | |
45 import java.util.regex.Pattern; | |
46 | |
47 import com.five_ten_sg.connectbot.R; | |
48 import com.five_ten_sg.connectbot.bean.HostBean; | |
49 import com.five_ten_sg.connectbot.bean.PortForwardBean; | |
50 import com.five_ten_sg.connectbot.bean.PubkeyBean; | |
51 import com.five_ten_sg.connectbot.service.TerminalBridge; | |
52 import com.five_ten_sg.connectbot.service.TerminalManager; | |
53 import com.five_ten_sg.connectbot.service.TerminalManager.KeyHolder; | |
54 import com.five_ten_sg.connectbot.util.HostDatabase; | |
55 import com.five_ten_sg.connectbot.util.PubkeyDatabase; | |
56 import com.five_ten_sg.connectbot.util.PubkeyUtils; | |
57 import android.content.Context; | |
58 import android.net.Uri; | |
59 import android.os.Environment; | |
60 import android.util.Log; | |
61 | |
62 import com.trilead.ssh2.AuthAgentCallback; | |
63 import com.trilead.ssh2.ChannelCondition; | |
64 import com.trilead.ssh2.Connection; | |
65 import com.trilead.ssh2.ConnectionInfo; | |
66 import com.trilead.ssh2.ConnectionMonitor; | |
67 import com.trilead.ssh2.DynamicPortForwarder; | |
68 import com.trilead.ssh2.InteractiveCallback; | |
69 import com.trilead.ssh2.KnownHosts; | |
70 import com.trilead.ssh2.LocalPortForwarder; | |
71 import com.trilead.ssh2.SCPClient; | |
72 import com.trilead.ssh2.ServerHostKeyVerifier; | |
73 import com.trilead.ssh2.Session; | |
74 import com.trilead.ssh2.HTTPProxyData; | |
75 import com.trilead.ssh2.HTTPProxyException; | |
76 import com.trilead.ssh2.crypto.PEMDecoder; | |
77 import com.trilead.ssh2.signature.DSASHA1Verify; | |
78 import com.trilead.ssh2.signature.RSASHA1Verify; | |
79 | |
80 /** | |
81 * @author Kenny Root | |
82 * | |
83 */ | |
84 public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveCallback, AuthAgentCallback { | |
85 public SSH() { | |
86 super(); | |
87 } | |
88 | |
89 /** | |
90 * @param bridge | |
91 * @param db | |
92 */ | |
93 public SSH(HostBean host, TerminalBridge bridge, TerminalManager manager) { | |
94 super(host, bridge, manager); | |
95 } | |
96 | |
97 private static final String PROTOCOL = "ssh"; | |
98 private static final String TAG = "ConnectBot.SSH"; | |
99 private static final int DEFAULT_PORT = 22; | |
100 | |
101 private static final String AUTH_PUBLICKEY = "publickey", | |
102 AUTH_PASSWORD = "password", | |
103 AUTH_KEYBOARDINTERACTIVE = "keyboard-interactive"; | |
104 | |
105 private final static int AUTH_TRIES = 20; | |
106 | |
107 static final Pattern hostmask; | |
108 static { | |
109 hostmask = Pattern.compile("^(.+)@([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE); | |
110 } | |
111 | |
112 private boolean compression = false; | |
113 private String httpproxy = null; | |
114 private volatile boolean authenticated = false; | |
115 private volatile boolean connected = false; | |
116 private volatile boolean sessionOpen = false; | |
117 | |
118 private boolean pubkeysExhausted = false; | |
119 private boolean interactiveCanContinue = true; | |
120 | |
121 private Connection connection; | |
122 private Session session; | |
123 private ConnectionInfo connectionInfo; | |
124 | |
125 private OutputStream stdin; | |
126 private InputStream stdout; | |
127 private InputStream stderr; | |
128 | |
129 private static final int conditions = ChannelCondition.STDOUT_DATA | |
130 | ChannelCondition.STDERR_DATA | |
131 | ChannelCondition.CLOSED | |
132 | ChannelCondition.EOF; | |
133 | |
134 private List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>(); | |
135 | |
136 private int columns; | |
137 private int rows; | |
138 | |
139 private int width; | |
140 private int height; | |
141 | |
142 private String useAuthAgent = HostDatabase.AUTHAGENT_NO; | |
143 private String agentLockPassphrase; | |
144 | |
145 public class HostKeyVerifier implements ServerHostKeyVerifier { | |
146 public boolean verifyServerHostKey(String hostname, int port, | |
147 String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException { | |
148 // read in all known hosts from hostdb | |
149 KnownHosts hosts = manager.hostdb.getKnownHosts(); | |
150 Boolean result; | |
151 String matchName = String.format(Locale.US, "%s:%d", hostname, port); | |
152 String fingerprint = KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey); | |
153 String algorithmName; | |
154 | |
155 if ("ssh-rsa".equals(serverHostKeyAlgorithm)) | |
156 algorithmName = "RSA"; | |
157 else if ("ssh-dss".equals(serverHostKeyAlgorithm)) | |
158 algorithmName = "DSA"; | |
159 else if (serverHostKeyAlgorithm.startsWith("ecdsa-")) | |
160 algorithmName = "EC"; | |
161 else | |
162 algorithmName = serverHostKeyAlgorithm; | |
163 | |
164 switch (hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) { | |
165 case KnownHosts.HOSTKEY_IS_OK: | |
166 bridge.outputLine(manager.res.getString(R.string.terminal_sucess, algorithmName, fingerprint)); | |
167 return true; | |
168 | |
169 case KnownHosts.HOSTKEY_IS_NEW: | |
170 // prompt user | |
171 bridge.outputLine(manager.res.getString(R.string.host_authenticity_warning, hostname)); | |
172 bridge.outputLine(manager.res.getString(R.string.host_fingerprint, algorithmName, fingerprint)); | |
173 result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting)); | |
174 | |
175 if (result == null) return false; | |
176 | |
177 if (result.booleanValue()) { | |
178 // save this key in known database | |
179 manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey); | |
180 } | |
181 | |
182 return result.booleanValue(); | |
183 | |
184 case KnownHosts.HOSTKEY_HAS_CHANGED: | |
185 String header = String.format("@ %s @", | |
186 manager.res.getString(R.string.host_verification_failure_warning_header)); | |
187 char[] atsigns = new char[header.length()]; | |
188 Arrays.fill(atsigns, '@'); | |
189 String border = new String(atsigns); | |
190 bridge.outputLine(border); | |
191 bridge.outputLine(manager.res.getString(R.string.host_verification_failure_warning)); | |
192 bridge.outputLine(border); | |
193 bridge.outputLine(String.format(manager.res.getString(R.string.host_fingerprint), | |
194 algorithmName, fingerprint)); | |
195 // Users have no way to delete keys, so we'll prompt them for now. | |
196 result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting)); | |
197 | |
198 if (result == null) return false; | |
199 | |
200 if (result.booleanValue()) { | |
201 // save this key in known database | |
202 manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey); | |
203 } | |
204 | |
205 return result.booleanValue(); | |
206 | |
207 default: | |
208 return false; | |
209 } | |
210 } | |
211 | |
212 } | |
213 | |
214 private void authenticate() { | |
215 try { | |
216 if (connection.authenticateWithNone(host.getUsername())) { | |
217 finishConnection(); | |
218 return; | |
219 } | |
220 } | |
221 catch (Exception e) { | |
222 Log.d(TAG, "Host does not support 'none' authentication."); | |
223 } | |
224 | |
225 bridge.outputLine(manager.res.getString(R.string.terminal_auth)); | |
226 | |
227 try { | |
228 long pubkeyId = host.getPubkeyId(); | |
229 | |
230 if (!pubkeysExhausted && | |
231 pubkeyId != HostDatabase.PUBKEYID_NEVER && | |
232 connection.isAuthMethodAvailable(host.getUsername(), AUTH_PUBLICKEY)) { | |
233 // if explicit pubkey defined for this host, then prompt for password as needed | |
234 // otherwise just try all in-memory keys held in terminalmanager | |
235 if (pubkeyId == HostDatabase.PUBKEYID_ANY) { | |
236 // try each of the in-memory keys | |
237 bridge.outputLine(manager.res | |
238 .getString(R.string.terminal_auth_pubkey_any)); | |
239 | |
240 for (Entry<String, KeyHolder> entry : manager.loadedKeypairs.entrySet()) { | |
241 if (entry.getValue().bean.isConfirmUse() | |
242 && !promptForPubkeyUse(entry.getKey())) | |
243 continue; | |
244 | |
245 if (this.tryPublicKey(host.getUsername(), entry.getKey(), | |
246 entry.getValue().pair)) { | |
247 finishConnection(); | |
248 break; | |
249 } | |
250 } | |
251 } | |
252 else { | |
253 bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_specific)); | |
254 // use a specific key for this host, as requested | |
255 PubkeyBean pubkey = manager.pubkeydb.findPubkeyById(pubkeyId); | |
256 | |
257 if (pubkey == null) | |
258 bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_invalid)); | |
259 else if (tryPublicKey(pubkey)) | |
260 finishConnection(); | |
261 } | |
262 | |
263 pubkeysExhausted = true; | |
264 } | |
265 else if (interactiveCanContinue && | |
266 connection.isAuthMethodAvailable(host.getUsername(), AUTH_KEYBOARDINTERACTIVE)) { | |
267 // this auth method will talk with us using InteractiveCallback interface | |
268 // it blocks until authentication finishes | |
269 bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki)); | |
270 interactiveCanContinue = false; | |
271 | |
272 if (connection.authenticateWithKeyboardInteractive(host.getUsername(), this)) { | |
273 finishConnection(); | |
274 } | |
275 else { | |
276 bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki_fail)); | |
277 } | |
278 } | |
279 else if (connection.isAuthMethodAvailable(host.getUsername(), AUTH_PASSWORD)) { | |
280 bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass)); | |
281 String password = bridge.getPromptHelper().requestPasswordPrompt(null, | |
282 manager.res.getString(R.string.prompt_password)); | |
283 | |
284 if (password != null | |
285 && connection.authenticateWithPassword(host.getUsername(), password)) { | |
286 finishConnection(); | |
287 } | |
288 else { | |
289 bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass_fail)); | |
290 } | |
291 } | |
292 else { | |
293 bridge.outputLine(manager.res.getString(R.string.terminal_auth_fail)); | |
294 } | |
295 } | |
296 catch (IllegalStateException e) { | |
297 Log.e(TAG, "Connection went away while we were trying to authenticate", e); | |
298 return; | |
299 } | |
300 catch (Exception e) { | |
301 Log.e(TAG, "Problem during handleAuthentication()", e); | |
302 } | |
303 } | |
304 | |
305 /** | |
306 * Attempt connection with database row pointed to by cursor. | |
307 * @param cursor | |
308 * @return true for successful authentication | |
309 * @throws NoSuchAlgorithmException | |
310 * @throws InvalidKeySpecException | |
311 * @throws IOException | |
312 */ | |
313 private boolean tryPublicKey(PubkeyBean pubkey) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException { | |
314 KeyPair pair = null; | |
315 | |
316 if (manager.isKeyLoaded(pubkey.getNickname())) { | |
317 // load this key from memory if its already there | |
318 Log.d(TAG, String.format("Found unlocked key '%s' already in-memory", pubkey.getNickname())); | |
319 | |
320 if (pubkey.isConfirmUse()) { | |
321 if (!promptForPubkeyUse(pubkey.getNickname())) | |
322 return false; | |
323 } | |
324 | |
325 pair = manager.getKey(pubkey.getNickname()); | |
326 } | |
327 else { | |
328 // otherwise load key from database and prompt for password as needed | |
329 String password = null; | |
330 | |
331 if (pubkey.isEncrypted()) { | |
332 password = bridge.getPromptHelper().requestPasswordPrompt(null, | |
333 manager.res.getString(R.string.prompt_pubkey_password, pubkey.getNickname())); | |
334 | |
335 // Something must have interrupted the prompt. | |
336 if (password == null) | |
337 return false; | |
338 } | |
339 | |
340 if (PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType())) { | |
341 // load specific key using pem format | |
342 pair = PEMDecoder.decode(new String(pubkey.getPrivateKey()).toCharArray(), password); | |
343 } | |
344 else { | |
345 // load using internal generated format | |
346 PrivateKey privKey; | |
347 | |
348 try { | |
349 privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), | |
350 pubkey.getType(), password); | |
351 } | |
352 catch (Exception e) { | |
353 String message = String.format("Bad password for key '%s'. Authentication failed.", pubkey.getNickname()); | |
354 Log.e(TAG, message, e); | |
355 bridge.outputLine(message); | |
356 return false; | |
357 } | |
358 | |
359 PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType()); | |
360 // convert key to trilead format | |
361 pair = new KeyPair(pubKey, privKey); | |
362 Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey)); | |
363 } | |
364 | |
365 Log.d(TAG, String.format("Unlocked key '%s'", pubkey.getNickname())); | |
366 // save this key in memory | |
367 manager.addKey(pubkey, pair); | |
368 } | |
369 | |
370 return tryPublicKey(host.getUsername(), pubkey.getNickname(), pair); | |
371 } | |
372 | |
373 private boolean tryPublicKey(String username, String keyNickname, KeyPair pair) throws IOException { | |
374 //bridge.outputLine(String.format("Attempting 'publickey' with key '%s' [%s]...", keyNickname, trileadKey.toString())); | |
375 boolean success = connection.authenticateWithPublicKey(username, pair); | |
376 | |
377 if (!success) | |
378 bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_fail, keyNickname)); | |
379 | |
380 return success; | |
381 } | |
382 | |
383 /** | |
384 * Internal method to request actual PTY terminal once we've finished | |
385 * authentication. If called before authenticated, it will just fail. | |
386 */ | |
387 private void finishConnection() { | |
388 authenticated = true; | |
389 | |
390 for (PortForwardBean portForward : portForwards) { | |
391 try { | |
392 enablePortForward(portForward); | |
393 bridge.outputLine(manager.res.getString(R.string.terminal_enable_portfoward, portForward.getDescription())); | |
394 } | |
395 catch (Exception e) { | |
396 Log.e(TAG, "Error setting up port forward during connect", e); | |
397 } | |
398 } | |
399 | |
400 if (!host.getWantSession()) { | |
401 bridge.outputLine(manager.res.getString(R.string.terminal_no_session)); | |
402 bridge.onConnected(); | |
403 return; | |
404 } | |
405 | |
406 try { | |
407 session = connection.openSession(); | |
408 | |
409 if (!useAuthAgent.equals(HostDatabase.AUTHAGENT_NO)) | |
410 session.requestAuthAgentForwarding(this); | |
411 | |
412 if (host.getWantX11Forward()) { | |
413 try { | |
414 session.requestX11Forwarding(host.getX11Host(), host.getX11Port(), null, false); | |
415 } | |
416 catch (IOException e2) { | |
417 Log.e(TAG, "Problem while trying to setup X11 forwarding in finishConnection()", e2); | |
418 } | |
419 } | |
420 | |
421 session.requestPTY(getEmulation(), columns, rows, width, height, null); | |
422 session.startShell(); | |
423 stdin = session.getStdin(); | |
424 stdout = session.getStdout(); | |
425 stderr = session.getStderr(); | |
426 sessionOpen = true; | |
427 bridge.onConnected(); | |
428 } | |
429 catch (IOException e1) { | |
430 Log.e(TAG, "Problem while trying to create PTY in finishConnection()", e1); | |
431 } | |
432 } | |
433 | |
434 @Override | |
435 public void connect() { | |
436 connection = new Connection(host.getHostname(), host.getPort()); | |
437 connection.addConnectionMonitor(this); | |
438 | |
439 try { | |
440 connection.setCompression(compression); | |
441 } | |
442 catch (IOException e) { | |
443 Log.e(TAG, "Could not enable compression!", e); | |
444 } | |
445 | |
446 if (httpproxy != null && httpproxy.length() > 0) { | |
447 Log.d(TAG, "Want HTTP Proxy: " + httpproxy, null); | |
448 | |
449 try { | |
450 URL u; | |
451 | |
452 if (httpproxy.startsWith("http://")) { | |
453 u = new URL(httpproxy); | |
454 } | |
455 else { | |
456 u = new URL("http://" + httpproxy); | |
457 } | |
458 | |
459 connection.setProxyData(new HTTPProxyData( | |
460 u.getHost(), | |
461 u.getPort(), | |
462 u.getUserInfo(), | |
463 u.getAuthority())); | |
464 bridge.outputLine("Connecting via proxy: " + httpproxy); | |
465 } | |
466 catch (MalformedURLException e) { | |
467 Log.e(TAG, "Could not parse proxy " + httpproxy, e); | |
468 // Display the reason in the text. | |
469 bridge.outputLine("Bad proxy URL: " + httpproxy); | |
470 onDisconnect(); | |
471 return; | |
472 } | |
473 } | |
474 | |
475 try { | |
476 /* Uncomment when debugging SSH protocol: | |
477 DebugLogger logger = new DebugLogger() { | |
478 | |
479 public void log(int level, String className, String message) { | |
480 Log.d("SSH", message); | |
481 } | |
482 | |
483 }; | |
484 Logger.enabled = true; | |
485 Logger.logger = logger; | |
486 */ | |
487 connectionInfo = connection.connect(new HostKeyVerifier()); | |
488 connected = true; | |
489 | |
490 if (connectionInfo.clientToServerCryptoAlgorithm | |
491 .equals(connectionInfo.serverToClientCryptoAlgorithm) | |
492 && connectionInfo.clientToServerMACAlgorithm | |
493 .equals(connectionInfo.serverToClientMACAlgorithm)) { | |
494 bridge.outputLine(manager.res.getString(R.string.terminal_using_algorithm, | |
495 connectionInfo.clientToServerCryptoAlgorithm, | |
496 connectionInfo.clientToServerMACAlgorithm)); | |
497 } | |
498 else { | |
499 bridge.outputLine(manager.res.getString( | |
500 R.string.terminal_using_c2s_algorithm, | |
501 connectionInfo.clientToServerCryptoAlgorithm, | |
502 connectionInfo.clientToServerMACAlgorithm)); | |
503 bridge.outputLine(manager.res.getString( | |
504 R.string.terminal_using_s2c_algorithm, | |
505 connectionInfo.serverToClientCryptoAlgorithm, | |
506 connectionInfo.serverToClientMACAlgorithm)); | |
507 } | |
508 } | |
509 catch (HTTPProxyException e) { | |
510 Log.e(TAG, "Failed to connect to HTTP Proxy", e); | |
511 // Display the reason in the text. | |
512 bridge.outputLine("Failed to connect to HTTP Proxy."); | |
513 onDisconnect(); | |
514 return; | |
515 } | |
516 catch (IOException e) { | |
517 Log.e(TAG, "Problem in SSH connection thread during authentication", e); | |
518 // Display the reason in the text. | |
519 bridge.outputLine(e.getCause().getMessage()); | |
520 onDisconnect(); | |
521 return; | |
522 } | |
523 | |
524 try { | |
525 // enter a loop to keep trying until authentication | |
526 int tries = 0; | |
527 | |
528 while (connected && !connection.isAuthenticationComplete() && tries++ < AUTH_TRIES) { | |
529 authenticate(); | |
530 // sleep to make sure we dont kill system | |
531 Thread.sleep(1000); | |
532 } | |
533 } | |
534 catch (Exception e) { | |
535 Log.e(TAG, "Problem in SSH connection thread during authentication", e); | |
536 } | |
537 } | |
538 | |
539 @Override | |
540 public void close() { | |
541 connected = false; | |
542 | |
543 if (session != null) { | |
544 session.close(); | |
545 session = null; | |
546 } | |
547 | |
548 if (connection != null) { | |
549 connection.close(); | |
550 connection = null; | |
551 } | |
552 } | |
553 | |
554 private void onDisconnect() { | |
555 close(); | |
556 bridge.dispatchDisconnect(false); | |
557 } | |
558 | |
559 @Override | |
560 public void flush() throws IOException { | |
561 if (stdin != null) | |
562 stdin.flush(); | |
563 } | |
564 | |
565 @Override | |
566 public boolean willBlock() { | |
567 if (stdout == null) return true; | |
568 try { | |
569 return stdout.available() == 0; | |
570 } catch (Exception e) { | |
571 return true; | |
572 } | |
573 } | |
574 | |
575 @Override | |
576 public int read(byte[] buffer, int start, int len) throws IOException { | |
577 int bytesRead = 0; | |
578 | |
579 if (session == null) | |
580 return 0; | |
581 | |
582 int newConditions = session.waitForCondition(conditions, 0); | |
583 | |
584 if ((newConditions & ChannelCondition.STDOUT_DATA) != 0) { | |
585 bytesRead = stdout.read(buffer, start, len); | |
586 } | |
587 | |
588 if ((newConditions & ChannelCondition.STDERR_DATA) != 0) { | |
589 byte discard[] = new byte[256]; | |
590 | |
591 while (stderr.available() > 0) { | |
592 stderr.read(discard); | |
593 } | |
594 } | |
595 | |
596 if ((newConditions & ChannelCondition.EOF) != 0) { | |
597 onDisconnect(); | |
598 throw new IOException("Remote end closed connection"); | |
599 } | |
600 | |
601 return bytesRead; | |
602 } | |
603 | |
604 @Override | |
605 public void write(byte[] buffer) throws IOException { | |
606 if (stdin != null) | |
607 stdin.write(buffer); | |
608 } | |
609 | |
610 @Override | |
611 public void write(int c) throws IOException { | |
612 if (stdin != null) | |
613 stdin.write(c); | |
614 } | |
615 | |
616 @Override | |
617 public Map<String, String> getOptions() { | |
618 Map<String, String> options = new HashMap<String, String>(); | |
619 options.put("compression", Boolean.toString(compression)); | |
620 | |
621 if (httpproxy != null) | |
622 options.put("httpproxy", httpproxy); | |
623 | |
624 return options; | |
625 } | |
626 | |
627 @Override | |
628 public void setOptions(Map<String, String> options) { | |
629 if (options.containsKey("compression")) | |
630 compression = Boolean.parseBoolean(options.get("compression")); | |
631 | |
632 if (options.containsKey("httpproxy")) | |
633 httpproxy = options.get("httpproxy"); | |
634 } | |
635 | |
636 public static String getProtocolName() { | |
637 return PROTOCOL; | |
638 } | |
639 | |
640 @Override | |
641 public boolean isSessionOpen() { | |
642 return sessionOpen; | |
643 } | |
644 | |
645 @Override | |
646 public boolean isConnected() { | |
647 return connected; | |
648 } | |
649 | |
650 @Override | |
651 public boolean isAuthenticated() { | |
652 return authenticated; | |
653 } | |
654 | |
655 public void connectionLost(Throwable reason) { | |
656 onDisconnect(); | |
657 } | |
658 | |
659 @Override | |
660 public boolean canForwardPorts() { | |
661 return true; | |
662 } | |
663 | |
664 @Override | |
665 public List<PortForwardBean> getPortForwards() { | |
666 return portForwards; | |
667 } | |
668 | |
669 @Override | |
670 public boolean addPortForward(PortForwardBean portForward) { | |
671 return portForwards.add(portForward); | |
672 } | |
673 | |
674 @Override | |
675 public boolean removePortForward(PortForwardBean portForward) { | |
676 // Make sure we don't have a phantom forwarder. | |
677 disablePortForward(portForward); | |
678 return portForwards.remove(portForward); | |
679 } | |
680 | |
681 @Override | |
682 public boolean enablePortForward(PortForwardBean portForward) { | |
683 if (!portForwards.contains(portForward)) { | |
684 Log.e(TAG, "Attempt to enable port forward not in list"); | |
685 return false; | |
686 } | |
687 | |
688 if (!authenticated) | |
689 return false; | |
690 | |
691 if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) { | |
692 LocalPortForwarder lpf = null; | |
693 | |
694 try { | |
695 lpf = connection.createLocalPortForwarder( | |
696 new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort()), | |
697 portForward.getDestAddr(), portForward.getDestPort()); | |
698 } | |
699 catch (Exception e) { | |
700 Log.e(TAG, "Could not create local port forward", e); | |
701 return false; | |
702 } | |
703 | |
704 if (lpf == null) { | |
705 Log.e(TAG, "returned LocalPortForwarder object is null"); | |
706 return false; | |
707 } | |
708 | |
709 portForward.setIdentifier(lpf); | |
710 portForward.setEnabled(true); | |
711 return true; | |
712 } | |
713 else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) { | |
714 try { | |
715 connection.requestRemotePortForwarding("", portForward.getSourcePort(), portForward.getDestAddr(), portForward.getDestPort()); | |
716 } | |
717 catch (Exception e) { | |
718 Log.e(TAG, "Could not create remote port forward", e); | |
719 return false; | |
720 } | |
721 | |
722 portForward.setEnabled(true); | |
723 return true; | |
724 } | |
725 else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) { | |
726 DynamicPortForwarder dpf = null; | |
727 | |
728 try { | |
729 dpf = connection.createDynamicPortForwarder( | |
730 new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort())); | |
731 } | |
732 catch (Exception e) { | |
733 Log.e(TAG, "Could not create dynamic port forward", e); | |
734 return false; | |
735 } | |
736 | |
737 portForward.setIdentifier(dpf); | |
738 portForward.setEnabled(true); | |
739 return true; | |
740 } | |
741 else { | |
742 // Unsupported type | |
743 Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType())); | |
744 return false; | |
745 } | |
746 } | |
747 | |
748 @Override | |
749 public boolean disablePortForward(PortForwardBean portForward) { | |
750 if (!portForwards.contains(portForward)) { | |
751 Log.e(TAG, "Attempt to disable port forward not in list"); | |
752 return false; | |
753 } | |
754 | |
755 if (!authenticated) | |
756 return false; | |
757 | |
758 if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) { | |
759 LocalPortForwarder lpf = null; | |
760 lpf = (LocalPortForwarder)portForward.getIdentifier(); | |
761 | |
762 if (!portForward.isEnabled() || lpf == null) { | |
763 Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname())); | |
764 return false; | |
765 } | |
766 | |
767 portForward.setEnabled(false); | |
768 | |
769 try { | |
770 lpf.close(); | |
771 } | |
772 catch (IOException e) { | |
773 Log.e(TAG, "Could not stop local port forwarder, setting enabled to false", e); | |
774 return false; | |
775 } | |
776 | |
777 return true; | |
778 } | |
779 else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) { | |
780 portForward.setEnabled(false); | |
781 | |
782 try { | |
783 connection.cancelRemotePortForwarding(portForward.getSourcePort()); | |
784 } | |
785 catch (IOException e) { | |
786 Log.e(TAG, "Could not stop remote port forwarding, setting enabled to false", e); | |
787 return false; | |
788 } | |
789 | |
790 return true; | |
791 } | |
792 else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) { | |
793 DynamicPortForwarder dpf = null; | |
794 dpf = (DynamicPortForwarder)portForward.getIdentifier(); | |
795 | |
796 if (!portForward.isEnabled() || dpf == null) { | |
797 Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname())); | |
798 return false; | |
799 } | |
800 | |
801 portForward.setEnabled(false); | |
802 | |
803 try { | |
804 dpf.close(); | |
805 } | |
806 catch (IOException e) { | |
807 Log.e(TAG, "Could not stop dynamic port forwarder, setting enabled to false", e); | |
808 return false; | |
809 } | |
810 | |
811 return true; | |
812 } | |
813 else { | |
814 // Unsupported type | |
815 Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType())); | |
816 return false; | |
817 } | |
818 } | |
819 | |
820 @Override | |
821 public boolean canTransferFiles() { | |
822 return true; | |
823 } | |
824 | |
825 @Override | |
826 public boolean downloadFile(String remoteFile, String localFolder) { | |
827 try { | |
828 SCPClient client = new SCPClient(connection); | |
829 | |
830 if (localFolder == null || localFolder == "") | |
831 localFolder = Environment.getExternalStorageDirectory().getAbsolutePath(); | |
832 | |
833 File dir = new File(localFolder); | |
834 dir.mkdirs(); | |
835 client.get(remoteFile, localFolder); | |
836 return true; | |
837 } | |
838 catch (IOException e) { | |
839 Log.e(TAG, "Could not download remote file", e); | |
840 return false; | |
841 } | |
842 } | |
843 | |
844 @Override | |
845 public boolean uploadFile(String localFile, String remoteFile, | |
846 String remoteFolder, String mode) { | |
847 try { | |
848 SCPClient client = new SCPClient(connection); | |
849 | |
850 if (remoteFolder == null) | |
851 remoteFolder = ""; | |
852 | |
853 if (remoteFile == null || remoteFile == "") | |
854 client.put(localFile, remoteFolder, mode); | |
855 else | |
856 client.put(localFile, remoteFile, remoteFolder, mode); | |
857 | |
858 return true; | |
859 } | |
860 catch (IOException e) { | |
861 Log.e(TAG, "Could not upload local file", e); | |
862 return false; | |
863 } | |
864 } | |
865 | |
866 @Override | |
867 public void setDimensions(int columns, int rows, int width, int height) { | |
868 this.columns = columns; | |
869 this.rows = rows; | |
870 | |
871 if (sessionOpen) { | |
872 try { | |
873 session.resizePTY(columns, rows, width, height); | |
874 } | |
875 catch (IOException e) { | |
876 Log.e(TAG, "Couldn't send resize PTY packet", e); | |
877 } | |
878 } | |
879 } | |
880 | |
881 @Override | |
882 public int getDefaultPort() { | |
883 return DEFAULT_PORT; | |
884 } | |
885 | |
886 @Override | |
887 public String getDefaultNickname(String username, String hostname, int port) { | |
888 if (port == DEFAULT_PORT) { | |
889 return String.format(Locale.US, "%s@%s", username, hostname); | |
890 } | |
891 else { | |
892 return String.format(Locale.US, "%s@%s:%d", username, hostname, port); | |
893 } | |
894 } | |
895 | |
896 public static Uri getUri(String input) { | |
897 Matcher matcher = hostmask.matcher(input); | |
898 | |
899 if (!matcher.matches()) | |
900 return null; | |
901 | |
902 StringBuilder sb = new StringBuilder(); | |
903 sb.append(PROTOCOL) | |
904 .append("://") | |
905 .append(Uri.encode(matcher.group(1))) | |
906 .append('@') | |
907 .append(matcher.group(2)); | |
908 String portString = matcher.group(4); | |
909 int port = DEFAULT_PORT; | |
910 | |
911 if (portString != null) { | |
912 try { | |
913 port = Integer.parseInt(portString); | |
914 | |
915 if (port < 1 || port > 65535) { | |
916 port = DEFAULT_PORT; | |
917 } | |
918 } | |
919 catch (NumberFormatException nfe) { | |
920 // Keep the default port | |
921 } | |
922 } | |
923 | |
924 if (port != DEFAULT_PORT) { | |
925 sb.append(':') | |
926 .append(port); | |
927 } | |
928 | |
929 sb.append("/#") | |
930 .append(Uri.encode(input)); | |
931 Uri uri = Uri.parse(sb.toString()); | |
932 return uri; | |
933 } | |
934 | |
935 /** | |
936 * Handle challenges from keyboard-interactive authentication mode. | |
937 */ | |
938 public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) { | |
939 interactiveCanContinue = true; | |
940 String[] responses = new String[numPrompts]; | |
941 | |
942 for (int i = 0; i < numPrompts; i++) { | |
943 // request response from user for each prompt | |
944 responses[i] = bridge.promptHelper.requestPasswordPrompt(instruction, prompt[i]); | |
945 } | |
946 | |
947 return responses; | |
948 } | |
949 | |
950 @Override | |
951 public HostBean createHost(Uri uri) { | |
952 HostBean host = new HostBean(); | |
953 host.setProtocol(PROTOCOL); | |
954 host.setHostname(uri.getHost()); | |
955 int port = uri.getPort(); | |
956 | |
957 if (port < 0) | |
958 port = DEFAULT_PORT; | |
959 | |
960 host.setPort(port); | |
961 host.setUsername(uri.getUserInfo()); | |
962 String nickname = uri.getFragment(); | |
963 | |
964 if (nickname == null || nickname.length() == 0) { | |
965 host.setNickname(getDefaultNickname(host.getUsername(), | |
966 host.getHostname(), host.getPort())); | |
967 } | |
968 else { | |
969 host.setNickname(uri.getFragment()); | |
970 } | |
971 | |
972 return host; | |
973 } | |
974 | |
975 @Override | |
976 public void getSelectionArgs(Uri uri, Map<String, String> selection) { | |
977 selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL); | |
978 selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment()); | |
979 selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost()); | |
980 int port = uri.getPort(); | |
981 | |
982 if (port < 0) | |
983 port = DEFAULT_PORT; | |
984 | |
985 selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port)); | |
986 selection.put(HostDatabase.FIELD_HOST_USERNAME, uri.getUserInfo()); | |
987 } | |
988 | |
989 @Override | |
990 public void setCompression(boolean compression) { | |
991 this.compression = compression; | |
992 } | |
993 | |
994 @Override | |
995 public void setHttpproxy(String httpproxy) { | |
996 this.httpproxy = httpproxy; | |
997 } | |
998 | |
999 public static String getFormatHint(Context context) { | |
1000 return String.format("%s@%s:%s", | |
1001 context.getString(R.string.format_username), | |
1002 context.getString(R.string.format_hostname), | |
1003 context.getString(R.string.format_port)); | |
1004 } | |
1005 | |
1006 @Override | |
1007 public void setUseAuthAgent(String useAuthAgent) { | |
1008 this.useAuthAgent = useAuthAgent; | |
1009 } | |
1010 | |
1011 public Map<String, byte[]> retrieveIdentities() { | |
1012 Map<String, byte[]> pubKeys = new HashMap<String, byte[]> (manager.loadedKeypairs.size()); | |
1013 | |
1014 for (Entry<String, KeyHolder> entry : manager.loadedKeypairs.entrySet()) { | |
1015 KeyPair pair = entry.getValue().pair; | |
1016 | |
1017 try { | |
1018 PrivateKey privKey = pair.getPrivate(); | |
1019 | |
1020 if (privKey instanceof RSAPrivateKey) { | |
1021 RSAPublicKey pubkey = (RSAPublicKey) pair.getPublic(); | |
1022 pubKeys.put(entry.getKey(), RSASHA1Verify.encodeSSHRSAPublicKey(pubkey)); | |
1023 } | |
1024 else if (privKey instanceof DSAPrivateKey) { | |
1025 DSAPublicKey pubkey = (DSAPublicKey) pair.getPublic(); | |
1026 pubKeys.put(entry.getKey(), DSASHA1Verify.encodeSSHDSAPublicKey(pubkey)); | |
1027 } | |
1028 else | |
1029 continue; | |
1030 } | |
1031 catch (IOException e) { | |
1032 continue; | |
1033 } | |
1034 } | |
1035 | |
1036 return pubKeys; | |
1037 } | |
1038 | |
1039 public KeyPair getKeyPair(byte[] publicKey) { | |
1040 String nickname = manager.getKeyNickname(publicKey); | |
1041 | |
1042 if (nickname == null) | |
1043 return null; | |
1044 | |
1045 if (useAuthAgent.equals(HostDatabase.AUTHAGENT_NO)) { | |
1046 Log.e(TAG, ""); | |
1047 return null; | |
1048 } | |
1049 else if (useAuthAgent.equals(HostDatabase.AUTHAGENT_CONFIRM) || | |
1050 manager.loadedKeypairs.get(nickname).bean.isConfirmUse()) { | |
1051 if (!promptForPubkeyUse(nickname)) | |
1052 return null; | |
1053 } | |
1054 | |
1055 return manager.getKey(nickname); | |
1056 } | |
1057 | |
1058 private boolean promptForPubkeyUse(String nickname) { | |
1059 Boolean result = bridge.promptHelper.requestBooleanPrompt(null, | |
1060 manager.res.getString(R.string.prompt_allow_agent_to_use_key, | |
1061 nickname)); | |
1062 return result; | |
1063 } | |
1064 | |
1065 public boolean addIdentity(KeyPair pair, String comment, boolean confirmUse, int lifetime) { | |
1066 PubkeyBean pubkey = new PubkeyBean(); | |
1067 // pubkey.setType(PubkeyDatabase.KEY_TYPE_IMPORTED); | |
1068 pubkey.setNickname(comment); | |
1069 pubkey.setConfirmUse(confirmUse); | |
1070 pubkey.setLifetime(lifetime); | |
1071 manager.addKey(pubkey, pair); | |
1072 return true; | |
1073 } | |
1074 | |
1075 public boolean removeAllIdentities() { | |
1076 manager.loadedKeypairs.clear(); | |
1077 return true; | |
1078 } | |
1079 | |
1080 public boolean removeIdentity(byte[] publicKey) { | |
1081 return manager.removeKey(publicKey); | |
1082 } | |
1083 | |
1084 public boolean isAgentLocked() { | |
1085 return agentLockPassphrase != null; | |
1086 } | |
1087 | |
1088 public boolean requestAgentUnlock(String unlockPassphrase) { | |
1089 if (agentLockPassphrase == null) | |
1090 return false; | |
1091 | |
1092 if (agentLockPassphrase.equals(unlockPassphrase)) | |
1093 agentLockPassphrase = null; | |
1094 | |
1095 return agentLockPassphrase == null; | |
1096 } | |
1097 | |
1098 public boolean setAgentLock(String lockPassphrase) { | |
1099 if (agentLockPassphrase != null) | |
1100 return false; | |
1101 | |
1102 agentLockPassphrase = lockPassphrase; | |
1103 return true; | |
1104 } | |
1105 | |
1106 /* (non-Javadoc) | |
1107 * @see com.five_ten_sg.connectbot.transport.AbsTransport#usesNetwork() | |
1108 */ | |
1109 @Override | |
1110 public boolean usesNetwork() { | |
1111 return true; | |
1112 } | |
1113 } |