diff --git a/app/build.gradle b/app/build.gradle index d2d093109..8d750d098 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,7 +59,7 @@ android { versionCode androidGitVersion.code() minSdkVersion 14 - targetSdkVersion 29 + targetSdkVersion 28 buildToolsVersion "29.0.3" ndkVersion "21.3.6528147" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6d7fc6a0b..40667ce30 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -70,6 +70,7 @@ + diff --git a/app/src/main/cpp/com_google_ase_Exec.cpp b/app/src/main/cpp/com_google_ase_Exec.cpp index 7784f7031..1ce183742 100644 --- a/app/src/main/cpp/com_google_ase_Exec.cpp +++ b/app/src/main/cpp/com_google_ase_Exec.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "android/log.h" @@ -130,6 +131,19 @@ static int create_subprocess( } } +JNIEXPORT jint JNICALL Java_com_google_ase_Exec_setenv( + JNIEnv* env, jclass clazz, jstring name, jstring value) { + char *name_8 = JNU_GetStringNativeChars(env, name); + char *value_8 = JNU_GetStringNativeChars(env, value); + + return setenv(name_8, value_8, 1); +} + +JNIEXPORT jint JNICALL Java_com_google_ase_Exec_kill( + JNIEnv * env, jclass clazz, jint pid, jint signal) { + return kill(pid, signal); +} + JNIEXPORT jobject JNICALL Java_com_google_ase_Exec_createSubprocess( JNIEnv* env, jclass clazz, jstring cmd, jstring arg0, jstring arg1, jintArray processIdArray) { diff --git a/app/src/main/cpp/com_google_ase_Exec.h b/app/src/main/cpp/com_google_ase_Exec.h index a2a605293..54ac2e015 100644 --- a/app/src/main/cpp/com_google_ase_Exec.h +++ b/app/src/main/cpp/com_google_ase_Exec.h @@ -23,6 +23,22 @@ JNIEXPORT jobject JNICALL Java_com_google_ase_Exec_createSubprocess JNIEXPORT void JNICALL Java_com_google_ase_Exec_setPtyWindowSize (JNIEnv *, jclass, jobject, jint, jint, jint, jint); +/* + * Class: com_google_ase_Exec + * Method: setenv + * Signature: (Ljava/lang/String;Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL Java_com_google_ase_Exec_setenv + (JNIEnv *, jclass, jstring, jstring); + +/* + * Class: com_google_ase_Exec + * Method: kill + * Signature: (II)I + */ +JNIEXPORT jint JNICALL Java_com_google_ase_Exec_kill + (JNIEnv *, jclass, jint, jint); + /* * Class: com_google_ase_Exec * Method: waitFor diff --git a/app/src/main/java/com/google/ase/Exec.java b/app/src/main/java/com/google/ase/Exec.java index 016fdf3c6..47dab3347 100644 --- a/app/src/main/java/com/google/ase/Exec.java +++ b/app/src/main/java/com/google/ase/Exec.java @@ -54,6 +54,10 @@ public static native FileDescriptor createSubprocess(String cmd, String arg0, St public static native void setPtyWindowSize(FileDescriptor fd, int row, int col, int xpixel, int ypixel); + public static native int setenv(String name, String value); + + public static native int kill(int pid, int signal); + /** * Causes the calling thread to wait for the process associated with the receiver to finish * executing. diff --git a/app/src/main/java/org/connectbot/ConsoleActivity.java b/app/src/main/java/org/connectbot/ConsoleActivity.java index b8530596f..8a1c0ae9c 100644 --- a/app/src/main/java/org/connectbot/ConsoleActivity.java +++ b/app/src/main/java/org/connectbot/ConsoleActivity.java @@ -27,6 +27,7 @@ import org.connectbot.service.TerminalBridge; import org.connectbot.service.TerminalKeyListener; import org.connectbot.service.TerminalManager; +import org.connectbot.util.InstallMosh; import org.connectbot.util.PreferenceConstants; import org.connectbot.util.TerminalViewPager; @@ -483,6 +484,10 @@ public void onCreate(Bundle icicle) { StrictModeSetup.run(); } + if (!InstallMosh.isInstallStarted()) { + new InstallMosh(this); + } + hardKeyboard = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY; diff --git a/app/src/main/java/org/connectbot/HostEditorFragment.java b/app/src/main/java/org/connectbot/HostEditorFragment.java index afdba1980..aa52b42f6 100644 --- a/app/src/main/java/org/connectbot/HostEditorFragment.java +++ b/app/src/main/java/org/connectbot/HostEditorFragment.java @@ -43,6 +43,7 @@ import org.connectbot.bean.HostBean; import org.connectbot.transport.SSH; +import org.connectbot.transport.Mosh; import org.connectbot.transport.Telnet; import org.connectbot.transport.TransportFactory; import org.connectbot.util.HostDatabase; @@ -506,6 +507,12 @@ private void setTransportType(String protocol, boolean setDefaultPortInModel) { mPortContainer.setVisibility(View.VISIBLE); mExpandCollapseButton.setVisibility(View.VISIBLE); mNicknameItem.setVisibility(View.VISIBLE); + } else if (Mosh.getProtocolName().equals(protocol)) { + mUsernameContainer.setVisibility(View.VISIBLE); + mHostnameContainer.setVisibility(View.VISIBLE); + mPortContainer.setVisibility(View.VISIBLE); + mExpandCollapseButton.setVisibility(View.VISIBLE); + mNicknameItem.setVisibility(View.VISIBLE); } else if (Telnet.getProtocolName().equals(protocol)) { mUsernameContainer.setVisibility(View.GONE); mHostnameContainer.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/org/connectbot/HostListActivity.java b/app/src/main/java/org/connectbot/HostListActivity.java index 6d816ce97..c81e0dca8 100644 --- a/app/src/main/java/org/connectbot/HostListActivity.java +++ b/app/src/main/java/org/connectbot/HostListActivity.java @@ -57,6 +57,7 @@ import org.connectbot.service.TerminalManager; import org.connectbot.transport.TransportFactory; import org.connectbot.util.HostDatabase; +import org.connectbot.util.InstallMosh; import org.connectbot.util.PreferenceConstants; import java.util.List; @@ -182,6 +183,9 @@ public void onCreate(Bundle icicle) { this.prefs = PreferenceManager.getDefaultSharedPreferences(this); + // install Mosh binaries + new InstallMosh(this); + // detect HTC Dream and apply special preferences if (Build.MANUFACTURER.equals("HTC") && Build.DEVICE.equals("dream")) { SharedPreferences.Editor editor = prefs.edit(); diff --git a/app/src/main/java/org/connectbot/bean/HostBean.java b/app/src/main/java/org/connectbot/bean/HostBean.java index 762897f29..1fd688791 100644 --- a/app/src/main/java/org/connectbot/bean/HostBean.java +++ b/app/src/main/java/org/connectbot/bean/HostBean.java @@ -19,6 +19,7 @@ import org.connectbot.transport.Local; import org.connectbot.transport.SSH; +import org.connectbot.transport.Mosh; import org.connectbot.transport.Telnet; import org.connectbot.transport.TransportFactory; import org.connectbot.util.HostDatabase; @@ -43,6 +44,8 @@ public class HostBean extends AbstractBean { private String hostname = null; private int port = 22; private String protocol = "ssh"; + private String hostKeyAlgo = null; + private byte[] hostKey = null; private long lastConnect = -1; private String color; private boolean useKeys = true; @@ -56,6 +59,9 @@ public class HostBean extends AbstractBean { private String encoding = HostDatabase.ENCODING_DEFAULT; private boolean stayConnected = false; private boolean quickDisconnect = false; + private int moshPort = -1; + private String moshServer = null; + private String locale = HostDatabase.LOCALE_DEFAULT; public HostBean() { @@ -113,6 +119,24 @@ public String getProtocol() { return protocol; } + public void setHostKeyAlgo(String hostKeyAlgo) { + this.hostKeyAlgo = hostKeyAlgo; + } + public String getHostKeyAlgo() { + return hostKeyAlgo; + } + public void setHostKey(byte[] hostKey) { + if (hostKey == null) + this.hostKey = null; + else + this.hostKey = hostKey.clone(); + } + public byte[] getHostKey() { + if (hostKey == null) + return null; + else + return hostKey.clone(); + } public void setLastConnect(long lastConnect) { this.lastConnect = lastConnect; } @@ -190,6 +214,30 @@ public boolean getStayConnected() { return stayConnected; } + public void setMoshPort(int moshPort) { + this.moshPort = moshPort; + } + + public int getMoshPort() { + return moshPort; + } + + public void setMoshServer(String moshServer) { + this.moshServer = moshServer; + } + + public String getMoshServer() { + return moshServer; + } + + public void setLocale(String locale) { + this.locale = locale; + } + + public String getLocale() { + return locale; + } + public void setQuickDisconnect(boolean quickDisconnect) { this.quickDisconnect = quickDisconnect; } @@ -230,6 +278,9 @@ public ContentValues getValues() { values.put(HostDatabase.FIELD_HOST_ENCODING, encoding); values.put(HostDatabase.FIELD_HOST_STAYCONNECTED, Boolean.toString(stayConnected)); values.put(HostDatabase.FIELD_HOST_QUICKDISCONNECT, Boolean.toString(quickDisconnect)); + values.put(HostDatabase.FIELD_HOST_MOSHPORT, moshPort); + values.put(HostDatabase.FIELD_HOST_MOSH_SERVER, moshServer); + values.put(HostDatabase.FIELD_HOST_LOCALE, locale); return values; } @@ -254,6 +305,9 @@ public static HostBean fromContentValues(ContentValues values) { host.setEncoding(values.getAsString(HostDatabase.FIELD_HOST_ENCODING)); host.setStayConnected(values.getAsBoolean(HostDatabase.FIELD_HOST_STAYCONNECTED)); host.setQuickDisconnect(values.getAsBoolean(HostDatabase.FIELD_HOST_QUICKDISCONNECT)); + host.setMoshPort(values.getAsInteger(HostDatabase.FIELD_HOST_MOSHPORT)); + host.setMoshServer(values.getAsString(HostDatabase.FIELD_HOST_MOSH_SERVER)); + host.setLocale(values.getAsString(HostDatabase.FIELD_HOST_LOCALE)); return host; } @@ -291,7 +345,19 @@ public boolean equals(Object o) { } else if (!hostname.equals(host.getHostname())) return false; - return port == host.getPort(); + if (port != host.getPort()) + return false; + + if (moshPort != host.getMoshPort()) + return false; + + if (moshServer != host.getMoshServer()) + return false; + + if (locale != host.getLocale()) + return false; + + return true; } @Override @@ -345,6 +411,15 @@ public String toString() { username.equals("") || hostname.equals("")) return ""; + if (port == defaultPort) + return username + "@" + hostname; + else + return username + "@" + hostname + ":" + port; + } else if (Mosh.getProtocolName().equals(protocol)) { + if (username == null || hostname == null || + username.equals("") || hostname.equals("")) + return ""; + if (port == defaultPort) return username + "@" + hostname; else diff --git a/app/src/main/java/org/connectbot/service/TerminalBridge.java b/app/src/main/java/org/connectbot/service/TerminalBridge.java index 744979c19..b38be3a3f 100644 --- a/app/src/main/java/org/connectbot/service/TerminalBridge.java +++ b/app/src/main/java/org/connectbot/service/TerminalBridge.java @@ -397,6 +397,10 @@ public void run() { * authentication. If called before authenticated, it will just fail. */ public void onConnected() { + onConnected(true); + } + + public void onConnected(boolean doPostLogin) { disconnected = false; ((vt320) buffer).reset(); @@ -429,6 +433,11 @@ public void onConnected() { injectString(host.getPostLogin()); } + public void postLogin() { + // send any post-login string, if requested + injectString(host.getPostLogin()); + } + /** * @return whether a session is open or not */ diff --git a/app/src/main/java/org/connectbot/service/TerminalManager.java b/app/src/main/java/org/connectbot/service/TerminalManager.java index 82524f13f..ad7aaa087 100644 --- a/app/src/main/java/org/connectbot/service/TerminalManager.java +++ b/app/src/main/java/org/connectbot/service/TerminalManager.java @@ -190,7 +190,11 @@ public void onDestroy() { /** * Disconnect all currently connected bridges. */ - public void disconnectAll(final boolean immediate, final boolean excludeLocal) { + private void disconnectAll(final boolean immediate) { + disconnectAll(immediate, false); + } + + public void disconnectAll(final boolean immediate, final boolean onlyRemote) { TerminalBridge[] tmpBridges = null; synchronized (bridges) { @@ -202,9 +206,9 @@ public void disconnectAll(final boolean immediate, final boolean excludeLocal) { if (tmpBridges != null) { // disconnect and dispose of any existing bridges for (int i = 0; i < tmpBridges.length; i++) { - if (excludeLocal && !tmpBridges[i].isUsingNetwork()) - continue; - tmpBridges[i].dispatchDisconnect(immediate); + if (tmpBridges[i].transport.resetOnConnectionChange() || !onlyRemote) { + tmpBridges[i].dispatchDisconnect(immediate); + } } } } @@ -519,6 +523,12 @@ public void onRebind(Intent intent) { Log.i(TAG, "Someone rebound to TerminalManager with " + bridges.size() + " bridges active"); keepServiceAlive(); setResizeAllowed(true); + + synchronized (bridges) { + for (TerminalBridge bridge : bridges) { + //bridge.onForeground(); + } + } } @Override @@ -527,12 +537,15 @@ public boolean onUnbind(Intent intent) { setResizeAllowed(true); - if (bridges.size() == 0) { - stopWithDelay(); - } else { - // tell each bridge to forget about their previous prompt handler - for (TerminalBridge bridge : bridges) { - bridge.promptHelper.setHandler(null); + synchronized (bridges) { + if (bridges.size() == 0) { + stopWithDelay(); + } else { + // tell each bridge to forget about their previous prompt handler + for (TerminalBridge bridge : bridges) { + bridge.promptHelper.setHandler(null); + //bridge.onBackground(); + } } } diff --git a/app/src/main/java/org/connectbot/transport/AbsTransport.java b/app/src/main/java/org/connectbot/transport/AbsTransport.java index e5a5cd745..17129162a 100644 --- a/app/src/main/java/org/connectbot/transport/AbsTransport.java +++ b/app/src/main/java/org/connectbot/transport/AbsTransport.java @@ -251,4 +251,18 @@ public static String getFormatHint(Context context) { * @return */ public abstract boolean usesNetwork(); + + public abstract boolean resetOnConnectionChange(); + + public void onBackground() { + } + + public void onForeground() { + } + + public void onScreenOff() { + } + + public void onScreenOn() { + } } diff --git a/app/src/main/java/org/connectbot/transport/Local.java b/app/src/main/java/org/connectbot/transport/Local.java index 06ee34288..59642b178 100644 --- a/app/src/main/java/org/connectbot/transport/Local.java +++ b/app/src/main/java/org/connectbot/transport/Local.java @@ -219,6 +219,11 @@ public boolean usesNetwork() { return false; } + @Override + public boolean resetOnConnectionChange() { + return false; + } + private interface Killer { void killProcess(int pid); } diff --git a/app/src/main/java/org/connectbot/transport/Mosh.java b/app/src/main/java/org/connectbot/transport/Mosh.java new file mode 100644 index 000000000..988698fd4 --- /dev/null +++ b/app/src/main/java/org/connectbot/transport/Mosh.java @@ -0,0 +1,563 @@ +/* + * Mosh support Copyright 2012 Daniel Drown + * + * Code based on ConnectBot's SSH client + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.transport; + +import java.io.IOException; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.regex.Matcher; +import java.net.InetAddress; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.UnknownHostException; + +import org.connectbot.R; +import org.connectbot.bean.HostBean; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.connectbot.util.InstallMosh; + +import android.net.Uri; +import android.util.Log; + +import com.trilead.ssh2.AuthAgentCallback; +import com.trilead.ssh2.ChannelCondition; +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.ConnectionMonitor; +import com.trilead.ssh2.InteractiveCallback; +import com.trilead.ssh2.ServerHostKeyVerifier; + +import com.google.ase.Exec; + +public class Mosh extends SSH implements ConnectionMonitor, InteractiveCallback, AuthAgentCallback { + private String moshPort, moshKey, moshIP; + private boolean sshDone = false; + private Integer moshPid; + + private FileDescriptor shellFd; + + private FileInputStream is; + private FileOutputStream os; + + public static final String PROTOCOL = "mosh"; + private static final String TAG = "ConnectBot.MOSH"; + private static final int DEFAULT_PORT = 22; + + private boolean stoppedForBackground = false; + + public Mosh() { + super(); + } + + /** + * @param bridge + * @param db + */ + public Mosh(HostBean host, TerminalBridge bridge, TerminalManager manager) { + super(host, bridge, manager); + } + + @Override + public void close() { + try { + if (os != null) { + os.close(); + os = null; + } + if (is != null) { + is.close(); + is = null; + } + } catch (IOException e) { + Log.e(TAG, "Couldn't close mosh", e); + } + + if (connected) + super.close(); + + if (moshPid != null) { + synchronized (moshPid) { + if (moshPid > 0) { + Exec.kill(moshPid, 18); // SIGCONT in case it's stopped + Exec.kill(moshPid, 15); // SIGTERM + } + } + } + } + + public void onBackground() { + if (sshDone) { + synchronized (moshPid) { + if (moshPid > 0) + Exec.kill(moshPid, 19); // SIGSTOP + stoppedForBackground = true; + } + } + } + + public void onForeground() { + if (sshDone) { + synchronized (moshPid) { + if (moshPid > 0) + Exec.kill(moshPid, 18); // SIGCONT + stoppedForBackground = false; + } + } + } + + public void onScreenOff() { + if (sshDone) { + synchronized (moshPid) { + if (moshPid > 0 && !stoppedForBackground) + Exec.kill(moshPid, 19); // SIGSTOP + } + } + } + + public void onScreenOn() { + if (sshDone) { + synchronized (moshPid) { + if (moshPid > 0 && !stoppedForBackground) + Exec.kill(moshPid, 18); // SIGCONT + } + } + } + + /** + * Internal method to request actual PTY terminal once we've finished + * authentication. If called before authenticated, it will just fail. + */ + @Override + protected void finishConnection() { + authenticated = true; + + try { + bridge.outputLine("trying to run mosh-server on the remote server"); + session = connection.openSession(); + + session.requestPTY("screen", 80, 25, 800, 600, null); + /* TODO: try { + session.sendEnvironment("LANG",host.getLocale()); + } catch(IOException e) { + bridge.outputLine("ssh rejected our LANG environment variable: "+e.getMessage()); + }*/ + + String serverCommand = host.getMoshServer(); + if (serverCommand == null) { + serverCommand = "mosh-server"; + } + serverCommand += " new -s -l LANG=" + host.getLocale(); + if (host.getMoshPort() > 0) { + serverCommand += " -p " + host.getMoshPort(); + } + session.execCommand(serverCommand); + + stdin = session.getStdin(); + stdout = session.getStdout(); + stderr = session.getStderr(); + + // means SSH session + sessionOpen = true; + + bridge.onConnected(false); + } catch (IOException e1) { + Log.e(TAG, "Problem while trying to create PTY in finishConnection()", e1); + } + } + + // use this class to pass the actual hostname to the actual HostKeyVerifier, otherwise it gets the raw IP + public class MoshHostKeyVerifier extends HostKeyVerifier implements ServerHostKeyVerifier { + String realHostname; + + public MoshHostKeyVerifier(String hostname) { + realHostname = hostname; + } + + public boolean verifyServerHostKey(String hostname, int port, + String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException { + return super.verifyServerHostKey(realHostname, port, serverHostKeyAlgorithm, serverHostKey); + } + } + + @Override + public void connect() { + if (!InstallMosh.isInstallStarted()) { + // check that InstallMosh was called by the Activity + bridge.outputLine("mosh-client binary install not started"); + onDisconnect(); + return; + } + if (!InstallMosh.isInstallDone()) { + bridge.outputLine("waiting for mosh binaries to install"); + InstallMosh.waitForInstall(); + } + + if (!InstallMosh.getMoshInstallStatus()) { + bridge.outputLine("mosh-client binary not found; install process failed"); + bridge.outputLine(InstallMosh.getInstallMessages()); + onDisconnect(); + return; + } + + bridge.outputLine(InstallMosh.getInstallMessages()); + + InetAddress addresses[]; + try { + addresses = InetAddress.getAllByName(host.getHostname()); + } catch (UnknownHostException e) { + bridge.outputLine("Launching mosh server via SSH failed, Unknown hostname: " + host.getHostname()); + + onDisconnect(); + return; + } + + moshIP = null; + int try_family = 4; + for (int i = 0; i < addresses.length || try_family == 4; i++) { + if (i == addresses.length) { + i = 0; + try_family = 6; + } + if (addresses.length == 0) { + break; + } + if (try_family == 4 && addresses[i] instanceof Inet4Address) { + moshIP = addresses[i].getHostAddress(); + break; + } + if (try_family == 6 && addresses[i] instanceof Inet6Address) { + moshIP = addresses[i].getHostAddress(); + break; + } + } + if (moshIP == null) { + bridge.outputLine("No address records found for hostname: " + host.getHostname()); + + onDisconnect(); + return; + } + bridge.outputLine("Mosh IP = " + moshIP); + + connection = new Connection(moshIP, host.getPort()); + connection.addConnectionMonitor(this); + + try { + connection.setCompression(compression); + } catch (IOException e) { + Log.e(TAG, "Could not enable compression!", e); + } + + try { + connectionInfo = connection.connect(new MoshHostKeyVerifier(host.getHostname())); + connected = true; + + if (connectionInfo.clientToServerCryptoAlgorithm + .equals(connectionInfo.serverToClientCryptoAlgorithm) + && connectionInfo.clientToServerMACAlgorithm + .equals(connectionInfo.serverToClientMACAlgorithm)) { + bridge.outputLine(manager.res.getString(R.string.terminal_using_algorithm, + connectionInfo.clientToServerCryptoAlgorithm, + connectionInfo.clientToServerMACAlgorithm)); + } else { + bridge.outputLine(manager.res.getString( + R.string.terminal_using_c2s_algorithm, + connectionInfo.clientToServerCryptoAlgorithm, + connectionInfo.clientToServerMACAlgorithm)); + + bridge.outputLine(manager.res.getString( + R.string.terminal_using_s2c_algorithm, + connectionInfo.serverToClientCryptoAlgorithm, + connectionInfo.serverToClientMACAlgorithm)); + } + } catch (IOException e) { + Log.e(TAG, "Problem in SSH connection thread during authentication", e); + + // Display the reason in the text. + bridge.outputLine(e.getCause().getMessage()); + + onDisconnect(); + return; + } + + try { + // enter a loop to keep trying until authentication + int tries = 0; + while (connected && !connection.isAuthenticationComplete() && tries++ < AUTH_TRIES) { + authenticate(); + + // sleep to make sure we dont kill system + Thread.sleep(1000); + } + } catch (Exception e) { + Log.e(TAG, "Problem in SSH connection thread during authentication", e); + } + } + + public String instanceProtocolName() { + return PROTOCOL; + } + + public static String getProtocolName() { + return PROTOCOL; + } + + @Override + public String getDefaultNickname(String username, String hostname, int port) { + if (port == DEFAULT_PORT) { + return String.format("mosh %s@%s", username, hostname); + } else { + return String.format("mosh %s@%s:%d", username, hostname, port); + } + } + + public static Uri getUri(String input) { + Matcher matcher = hostmask.matcher(input); + + if (!matcher.matches()) + return null; + + StringBuilder sb = new StringBuilder(); + StringBuilder nickname = new StringBuilder(); + + String username = matcher.group(1); + String hostname = matcher.group(2); + + sb.append(getProtocolName()) + .append("://") + .append(Uri.encode(username)) + .append('@') + .append(hostname); + nickname.append("mosh " + username + "@" + hostname); + + String portString = matcher.group(4); + int port = DEFAULT_PORT; + if (portString != null) { + try { + port = Integer.parseInt(portString); + if (port < 1 || port > 65535) { + port = DEFAULT_PORT; + } + } catch (NumberFormatException nfe) { + // Keep the default port + } + } + + if (port != DEFAULT_PORT) { + sb.append(':') + .append(port); + nickname.append(":" + port); + } + + sb.append("/#") + .append(Uri.encode(nickname.toString())); + + Uri uri = Uri.parse(sb.toString()); + + return uri; + } + + @Override + public void flush() throws IOException { + if (sshDone) { + os.flush(); + } else { + super.flush(); + } + } + + @Override + public boolean isConnected() { + if (sshDone) { + return is != null && os != null; + } else { + return super.isConnected(); + } + } + + @Override + public void connectionLost(Throwable reason) { + if (!sshDone) + onDisconnect(); + } + + @Override + public boolean isSessionOpen() { + if (sshDone) { + return is != null && os != null; + } else { + return super.isSessionOpen(); + } + } + + private void launchMosh() { + int[] pids = new int[1]; + + Exec.setenv("MOSH_KEY", moshKey); + Exec.setenv("TERM", getEmulation()); + Exec.setenv("TERMINFO", InstallMosh.getTerminfoPath()); + try { + shellFd = Exec.createSubprocess(InstallMosh.getMoshPath(), moshIP, moshPort, pids); + bridge.outputLine("[" + pids[0] + "]: " + InstallMosh.getMoshPath() + " " + moshIP + " " + moshPort); + Exec.setPtyWindowSize(shellFd, rows, columns, width, height); + } catch (Exception e) { + bridge.outputLine("failed to start mosh-client: " + e.toString()); + Log.e(TAG, "Cannot start mosh-client", e); + onDisconnect(); + return; + } finally { + Exec.setenv("MOSH_KEY", ""); + } + + moshPid = pids[0]; + Runnable exitWatcher = new Runnable() { + public void run() { + Exec.waitFor(moshPid); + synchronized (moshPid) { + moshPid = 0; + } + ; + + bridge.dispatchDisconnect(false); + } + }; + + Thread exitWatcherThread = new Thread(exitWatcher); + exitWatcherThread.setName("LocalExitWatcher"); + exitWatcherThread.setDaemon(true); + exitWatcherThread.start(); + + is = new FileInputStream(shellFd); + os = new FileOutputStream(shellFd); + + bridge.postLogin(); + } + + @Override + public int read(byte[] buffer, int start, int len) throws IOException { + if (sshDone) { + return mosh_read(buffer, start, len); + } else { + return ssh_read(buffer, start, len); + } + } + + private int mosh_read(byte[] buffer, int start, int len) throws IOException { + if (is == null) { + bridge.dispatchDisconnect(false); + throw new IOException("session closed"); + } + return is.read(buffer, start, len); + } + + private int ssh_read(byte[] buffer, int start, int len) throws IOException { + int bytesRead = 0; + + if (session == null) + return 0; + + int newConditions = session.waitForCondition(conditions, 0); + + if ((newConditions & ChannelCondition.STDOUT_DATA) != 0) { + bytesRead = stdout.read(buffer, start, len); + String connectTag = "MOSH CONNECT"; + String data = new String(buffer); + int connectOffset = data.indexOf(connectTag); + + if (connectOffset > -1) { + int connectDataOffset = connectOffset + connectTag.length() + 1; + int end = data.indexOf(" ", connectDataOffset); + if (end > -1) { + moshPort = data.substring(connectDataOffset, end); + int keyEnd = data.indexOf("\n", end + 1); + if (keyEnd > -1) { + moshKey = data.substring(end + 1, keyEnd - 1); + sshDone = true; + launchMosh(); + } + } + } + } + + if ((newConditions & ChannelCondition.STDERR_DATA) != 0) { + byte discard[] = new byte[256]; + while (stderr.available() > 0) { + stderr.read(discard); + } + } + + if ((newConditions & ChannelCondition.EOF) != 0) { + if (!sshDone) { + onDisconnect(); + throw new IOException("Remote end closed connection"); + } + } + + return bytesRead; + } + + @Override + public void setDimensions(int columns, int rows, int width, int height) { + if (sshDone) { + try { + Exec.setPtyWindowSize(shellFd, rows, columns, width, height); + } catch (Exception e) { + Log.e(TAG, "Couldn't resize pty", e); + } + } else { + super.setDimensions(columns, rows, width, height); + } + } + + @Override + public void write(byte[] buffer) throws IOException { + if (sshDone) { + if (os != null) + os.write(buffer); + } else { + super.write(buffer); + } + } + + @Override + public void write(int c) throws IOException { + if (sshDone) { + if (os != null) + os.write(c); + } else { + super.write(c); + } + } + + @Override + public boolean canForwardPorts() { + return false; + } + + @Override + public boolean usesNetwork() { + return true; // don't hold wifilock + } + + @Override + public boolean resetOnConnectionChange() { + return false; + } +} diff --git a/app/src/main/java/org/connectbot/transport/SSH.java b/app/src/main/java/org/connectbot/transport/SSH.java index 399d10887..84edf4727 100644 --- a/app/src/main/java/org/connectbot/transport/SSH.java +++ b/app/src/main/java/org/connectbot/transport/SSH.java @@ -111,38 +111,39 @@ public SSH(HostBean host, TerminalBridge bridge, TerminalManager manager) { AUTH_PASSWORD = "password", AUTH_KEYBOARDINTERACTIVE = "keyboard-interactive"; - private final static int AUTH_TRIES = 20; + protected final static int AUTH_TRIES = 20; - private static final Pattern hostmask = Pattern.compile( + static final Pattern hostmask = Pattern.compile( "^(.+)@(([0-9a-z.-]+)|(\\[[a-f:0-9]+\\]))(:(\\d+))?$", Pattern.CASE_INSENSITIVE); - private boolean compression = false; - private volatile boolean authenticated = false; - private volatile boolean connected = false; - private volatile boolean sessionOpen = false; + protected boolean compression = false; + protected volatile boolean authenticated = false; + protected volatile boolean connected = false; + protected volatile boolean sessionOpen = false; private boolean pubkeysExhausted = false; private boolean interactiveCanContinue = true; - private Connection connection; - private Session session; + protected Connection connection; + protected Session session; + protected ConnectionInfo connectionInfo; - private OutputStream stdin; - private InputStream stdout; - private InputStream stderr; + protected OutputStream stdin; + protected InputStream stdout; + protected InputStream stderr; - private static final int conditions = ChannelCondition.STDOUT_DATA + protected static final int conditions = ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA | ChannelCondition.CLOSED | ChannelCondition.EOF; private List portForwards = new ArrayList<>(); - private int columns; - private int rows; + protected int columns; + protected int rows; - private int width; - private int height; + protected int width; + protected int height; private String useAuthAgent = HostDatabase.AUTHAGENT_NO; private String agentLockPassphrase; @@ -241,7 +242,7 @@ public void addServerHostKey(String host, int port, String algorithm, byte[] hos } } - private void authenticate() { + protected void authenticate() { try { if (connection.authenticateWithNone(host.getUsername())) { finishConnection(); @@ -415,7 +416,7 @@ private boolean tryPublicKey(String username, String keyNickname, KeyPair pair) * Internal method to request actual PTY terminal once we've finished * authentication. If called before authenticated, it will just fail. */ - private void finishConnection() { + protected void finishConnection() { authenticated = true; for (PortForwardBean portForward : portForwards) { @@ -545,7 +546,7 @@ public void close() { } } - private void onDisconnect() { + protected void onDisconnect() { bridge.dispatchDisconnect(false); } @@ -611,6 +612,10 @@ public void setOptions(Map options) { compression = Boolean.parseBoolean(options.get("compression")); } + public String instanceProtocolName() { + return PROTOCOL; + } + public static String getProtocolName() { return PROTOCOL; } @@ -805,7 +810,7 @@ public static Uri getUri(String input) { StringBuilder sb = new StringBuilder(); - sb.append(PROTOCOL) + sb.append(getProtocolName()) .append("://") .append(Uri.encode(matcher.group(1))) .append('@') @@ -855,7 +860,7 @@ public String[] replyToChallenge(String name, String instruction, int numPrompts public HostBean createHost(Uri uri) { HostBean host = new HostBean(); - host.setProtocol(PROTOCOL); + host.setProtocol(instanceProtocolName()); host.setHostname(uri.getHost()); @@ -879,7 +884,7 @@ public HostBean createHost(Uri uri) { @Override public void getSelectionArgs(Uri uri, Map selection) { - selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL); + selection.put(HostDatabase.FIELD_HOST_PROTOCOL, instanceProtocolName()); selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment()); selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost()); @@ -1017,4 +1022,9 @@ public boolean setAgentLock(String lockPassphrase) { public boolean usesNetwork() { return true; } + + @Override + public boolean resetOnConnectionChange() { + return true; + } } diff --git a/app/src/main/java/org/connectbot/transport/Telnet.java b/app/src/main/java/org/connectbot/transport/Telnet.java index 4d68e35af..c92ea4aa0 100644 --- a/app/src/main/java/org/connectbot/transport/Telnet.java +++ b/app/src/main/java/org/connectbot/transport/Telnet.java @@ -346,4 +346,9 @@ public static String getFormatHint(Context context) { public boolean usesNetwork() { return true; } + + @Override + public boolean resetOnConnectionChange() { + return true; + } } diff --git a/app/src/main/java/org/connectbot/transport/TransportFactory.java b/app/src/main/java/org/connectbot/transport/TransportFactory.java index f7de7265e..cce965af2 100644 --- a/app/src/main/java/org/connectbot/transport/TransportFactory.java +++ b/app/src/main/java/org/connectbot/transport/TransportFactory.java @@ -36,9 +36,10 @@ public class TransportFactory { private static final String TAG = "CB.TransportFactory"; private static String[] transportNames = { - SSH.getProtocolName(), - Telnet.getProtocolName(), - Local.getProtocolName(), + SSH.getProtocolName(), + Mosh.getProtocolName(), + Telnet.getProtocolName(), + Local.getProtocolName(), }; /** @@ -48,6 +49,8 @@ public class TransportFactory { public static AbsTransport getTransport(String protocol) { if (SSH.getProtocolName().equals(protocol)) { return new SSH(); + } else if (Mosh.getProtocolName().equals(protocol)) { + return new Mosh(); } else if (Telnet.getProtocolName().equals(protocol)) { return new Telnet(); } else if (Local.getProtocolName().equals(protocol)) { @@ -63,6 +66,8 @@ public static Uri getUri(String scheme, String input) { input)); if (SSH.getProtocolName().equals(scheme)) return SSH.getUri(input); + else if (Mosh.getProtocolName().equals(scheme)) + return Mosh.getUri(input); else if (Telnet.getProtocolName().equals(scheme)) return Telnet.getUri(input); else if (Local.getProtocolName().equals(scheme)) { @@ -96,6 +101,8 @@ public static boolean canForwardPorts(String protocol) { public static String getFormatHint(String protocol, Context context) { if (SSH.getProtocolName().equals(protocol)) { return SSH.getFormatHint(context); + } else if (Mosh.getProtocolName().equals(protocol)) { + return Mosh.getFormatHint(context); } else if (Telnet.getProtocolName().equals(protocol)) { return Telnet.getFormatHint(context); } else if (Local.getProtocolName().equals(protocol)) { diff --git a/app/src/main/java/org/connectbot/util/HostDatabase.java b/app/src/main/java/org/connectbot/util/HostDatabase.java index 8b7a433b9..b6e603107 100644 --- a/app/src/main/java/org/connectbot/util/HostDatabase.java +++ b/app/src/main/java/org/connectbot/util/HostDatabase.java @@ -52,7 +52,7 @@ public class HostDatabase extends RobustSQLiteOpenHelper implements HostStorage, public final static String TAG = "CB.HostDatabase"; public final static String DB_NAME = "hosts"; - public final static int DB_VERSION = 26; + public final static int DB_VERSION = 27; public final static String TABLE_HOSTS = "hosts"; public final static String FIELD_HOST_NICKNAME = "nickname"; @@ -73,6 +73,9 @@ public class HostDatabase extends RobustSQLiteOpenHelper implements HostStorage, public final static String FIELD_HOST_ENCODING = "encoding"; public final static String FIELD_HOST_STAYCONNECTED = "stayconnected"; public final static String FIELD_HOST_QUICKDISCONNECT = "quickdisconnect"; + public final static String FIELD_HOST_MOSHPORT = "moshport"; + public final static String FIELD_HOST_MOSH_SERVER = "moshserver"; + public final static String FIELD_HOST_LOCALE = "locale"; public final static String TABLE_KNOWNHOSTS = "knownhosts"; public final static String FIELD_KNOWNHOSTS_HOSTID = "hostid"; @@ -117,6 +120,8 @@ public class HostDatabase extends RobustSQLiteOpenHelper implements HostStorage, public final static String AUTHAGENT_YES = "yes"; public final static String ENCODING_DEFAULT = Charset.defaultCharset().name(); + public final static String LOCALE_DEFAULT = "en_US.UTF-8"; + public final static String MOSH_SERVER_DEFAULT = "mosh-server"; public final static long PUBKEYID_NEVER = -2; public final static long PUBKEYID_ANY = -1; @@ -142,7 +147,10 @@ public class HostDatabase extends RobustSQLiteOpenHelper implements HostStorage, + FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "', " + FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "', " + FIELD_HOST_STAYCONNECTED + " TEXT DEFAULT '" + Boolean.toString(false) + "', " - + FIELD_HOST_QUICKDISCONNECT + " TEXT DEFAULT '" + Boolean.toString(false) + "'"; + + FIELD_HOST_QUICKDISCONNECT + " TEXT DEFAULT '" + Boolean.toString(false) + "'" + + FIELD_HOST_MOSHPORT + " INTEGER DEFAULT 0, " + + FIELD_HOST_MOSH_SERVER + " TEXT DEFAULT '" + MOSH_SERVER_DEFAULT + "' " + + FIELD_HOST_LOCALE + " TEXT DEFAULT '" + LOCALE_DEFAULT + "', "; public static final String CREATE_TABLE_HOSTS = "CREATE TABLE " + TABLE_HOSTS + " (" + TABLE_HOSTS_COLUMNS + ")"; @@ -229,7 +237,7 @@ private void createTables(SQLiteDatabase db) { + FIELD_PORTFORWARD_TYPE + " TEXT NOT NULL DEFAULT '" + PORTFORWARD_LOCAL + "', " + FIELD_PORTFORWARD_SOURCEPORT + " INTEGER NOT NULL DEFAULT 8080, " + FIELD_PORTFORWARD_DESTADDR + " TEXT, " - + FIELD_PORTFORWARD_DESTPORT + " TEXT)"); + + FIELD_PORTFORWARD_DESTPORT + " TEXT, "); db.execSQL("CREATE INDEX " + TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index ON " + TABLE_PORTFORWARDS + " (" + FIELD_PORTFORWARD_HOSTID + ");"); @@ -399,6 +407,14 @@ public void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) t // fall through case 25: // TermBot update with agents, no longer in this new version based on Hardware Security SDK + case 26: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_MOSHPORT + " INTEGER DEFAULT 0"); + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_MOSH_SERVER + " TEXT DEFAULT '" + MOSH_SERVER_DEFAULT + "'"); + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_LOCALE + " TEXT DEFAULT '" + LOCALE_DEFAULT + "'"); + // fall through } } @@ -500,16 +516,19 @@ private List createHostBeans(Cursor c) { COL_LASTCONNECT = c.getColumnIndexOrThrow(FIELD_HOST_LASTCONNECT), COL_COLOR = c.getColumnIndexOrThrow(FIELD_HOST_COLOR), COL_USEKEYS = c.getColumnIndexOrThrow(FIELD_HOST_USEKEYS), - COL_USEAUTHAGENT = c.getColumnIndexOrThrow(FIELD_HOST_USEAUTHAGENT), - COL_POSTLOGIN = c.getColumnIndexOrThrow(FIELD_HOST_POSTLOGIN), - COL_PUBKEYID = c.getColumnIndexOrThrow(FIELD_HOST_PUBKEYID), - COL_WANTSESSION = c.getColumnIndexOrThrow(FIELD_HOST_WANTSESSION), - COL_DELKEY = c.getColumnIndexOrThrow(FIELD_HOST_DELKEY), - COL_FONTSIZE = c.getColumnIndexOrThrow(FIELD_HOST_FONTSIZE), - COL_COMPRESSION = c.getColumnIndexOrThrow(FIELD_HOST_COMPRESSION), - COL_ENCODING = c.getColumnIndexOrThrow(FIELD_HOST_ENCODING), - COL_STAYCONNECTED = c.getColumnIndexOrThrow(FIELD_HOST_STAYCONNECTED), - COL_QUICKDISCONNECT = c.getColumnIndexOrThrow(FIELD_HOST_QUICKDISCONNECT); + COL_USEAUTHAGENT = c.getColumnIndexOrThrow(FIELD_HOST_USEAUTHAGENT), + COL_POSTLOGIN = c.getColumnIndexOrThrow(FIELD_HOST_POSTLOGIN), + COL_PUBKEYID = c.getColumnIndexOrThrow(FIELD_HOST_PUBKEYID), + COL_WANTSESSION = c.getColumnIndexOrThrow(FIELD_HOST_WANTSESSION), + COL_DELKEY = c.getColumnIndexOrThrow(FIELD_HOST_DELKEY), + COL_FONTSIZE = c.getColumnIndexOrThrow(FIELD_HOST_FONTSIZE), + COL_COMPRESSION = c.getColumnIndexOrThrow(FIELD_HOST_COMPRESSION), + COL_ENCODING = c.getColumnIndexOrThrow(FIELD_HOST_ENCODING), + COL_STAYCONNECTED = c.getColumnIndexOrThrow(FIELD_HOST_STAYCONNECTED), + COL_QUICKDISCONNECT = c.getColumnIndexOrThrow(FIELD_HOST_QUICKDISCONNECT), + COL_MOSHPORT = c.getColumnIndexOrThrow(FIELD_HOST_MOSHPORT), + COL_MOSH_SERVER = c.getColumnIndexOrThrow(FIELD_HOST_MOSH_SERVER), + COL_LOCALE = c.getColumnIndexOrThrow(FIELD_HOST_LOCALE); while (c.moveToNext()) { HostBean host = new HostBean(); @@ -533,6 +552,9 @@ private List createHostBeans(Cursor c) { host.setEncoding(c.getString(COL_ENCODING)); host.setStayConnected(Boolean.valueOf(c.getString(COL_STAYCONNECTED))); host.setQuickDisconnect(Boolean.valueOf(c.getString(COL_QUICKDISCONNECT))); + host.setMoshPort(c.getInt(COL_MOSHPORT)); + host.setMoshServer(c.getString(COL_MOSH_SERVER)); + host.setLocale(c.getString(COL_LOCALE)); hosts.add(host); } diff --git a/app/src/main/java/org/connectbot/util/InstallMosh.java b/app/src/main/java/org/connectbot/util/InstallMosh.java new file mode 100644 index 000000000..d9699ad95 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/InstallMosh.java @@ -0,0 +1,273 @@ +/* + * Mosh support Copyright 2012 Daniel Drown + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import java.io.IOException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.MessageFormat; + +import org.connectbot.R; +import org.connectbot.util.PreferenceConstants; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.preference.PreferenceManager; +import android.os.Build; + +public final class InstallMosh implements Runnable { + private File data_dir; + private File bindir; + private File sharedir; + private Context context; + + private final static String BINARY_VERSION = "1.5"; + + // using installMessage as the object to lock to access static properties + private static StringBuilder installMessage = new StringBuilder(); + private static String moshPath = null; + private static String terminfoPath = null; + private static boolean installStarted = false; + private static boolean installDone = false; + private static boolean installFailed; + + public InstallMosh(Context context) { + this.context = context; + String path = MessageFormat.format("/data/data/{0}/files/", context.getApplicationInfo().packageName); + data_dir = new File(path); // hard-coded in binary packages + bindir = new File(data_dir, "bin"); + sharedir = new File(data_dir, "share"); + File moshFile = new File(bindir, "mosh-client"); + File terminfoDir = new File(sharedir, "terminfo"); + + synchronized (installMessage) { + moshPath = moshFile.getPath(); + terminfoPath = terminfoDir.getPath(); + installStarted = true; + } + Thread installThread = new Thread(this); + installThread.setName("Install Thread"); + installThread.start(); + } + + public void run() { + boolean installStatus = install(); + synchronized (installMessage) { + installFailed = installStatus; + installDone = true; + installMessage.notifyAll(); + } + } + + public static String getTerminfoPath() { + synchronized (installMessage) { + return terminfoPath; + } + } + + public static String getMoshPath() { + synchronized (installMessage) { + return moshPath; + } + } + + public static boolean getMoshInstallStatus() { + String path = getMoshPath(); + if (path == null) { + throw new NullPointerException("no mosh path - was InstallMosh called?"); + } + File moshFile = new File(path); + return moshFile.exists(); + } + + public static void waitForInstall() { + synchronized(installMessage) { + while(installDone != true) { + try { + installMessage.wait(); + } catch(java.lang.InterruptedException e) { + return; + } + } + } + return; + } + + public static boolean isInstallStarted() { + synchronized(installMessage) { + return installStarted; + } + } + + public static boolean isInstallDone() { + synchronized(installMessage) { + return installDone; + } + } + + public static String getInstallMessages() { + synchronized(installMessage) { + return installMessage.toString(); + } + } + + public boolean install() { + if(!data_dir.exists()) { + if(!data_dir.mkdir()) { + installMessage.append("mkdir "+data_dir.getPath()+" failed\r\n"); + return false; + } + installMessage.append("mkdir "+data_dir.getPath()+": done\r\n"); + } + + if(!bindir.exists()) { + if(!bindir.mkdir()) { + installMessage.append("mkdir "+bindir.getPath()+" failed\r\n"); + return false; + } + installMessage.append("mkdir "+bindir.getPath()+": done\r\n"); + } + + if(!installBusybox()) + return false; + return installMosh(); + } + + private boolean installMosh() { + File mosh_client_path = new File(moshPath); + File busybox_path = new File(bindir, "busybox"); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String moshVersion = prefs.getString(PreferenceConstants.INSTALLED_MOSH_VERSION, ""); + + if(!mosh_client_path.exists() || !moshVersion.equals(BINARY_VERSION)) { + installMessage.append("installing mosh-client binary\r\n"); + try { + InputStream bin_tar = context.getResources().openRawResource(R.raw.mosh_tar_gz); + Process untar = Runtime.getRuntime().exec(busybox_path.getPath() + " tar -C " + data_dir + " -zxf -"); + OutputStream tar_out = untar.getOutputStream(); + InputStream stdout = untar.getInputStream(); + InputStream stderr = untar.getErrorStream(); + byte[] buffer = new byte[4096]; + int num; + + while((num = bin_tar.read(buffer)) > 0) { + tar_out.write(buffer, 0, num); + if(stdout.available() > 0) { + byte[] std_str = new byte[4096]; + num = stdout.read(std_str); + if(num > 0) { + installMessage.append("untar: "+new String(std_str, 0, num)+"\r\n"); + } + } + if(stderr.available() > 0) { + byte[] err_str = new byte[4096]; + num = stderr.read(err_str); + if(num > 0) { + installMessage.append("untar/error: "+new String(err_str, 0, num)+"\r\n"); + } + } + } + bin_tar.close(); + tar_out.close(); + if(untar.waitFor() > 0) { + installMessage.append("mosh binary install failed/untar: exit status != 0\r\n"); + return false; + } + } catch (Exception e) { + installMessage.append("mosh binary install failed/untar: "+e.toString()+"\r\n"); + return false; + } + try { + String binarytype = use_pie() ? "arm.pie" : "arm.nopie"; + Process ln_s = Runtime.getRuntime().exec(busybox_path.getPath()+" ln -sf "+data_dir+"/bin/mosh-client."+binarytype+" "+data_dir+"/bin/mosh-client"); + if(ln_s.waitFor() > 0) { + installMessage.append("mosh binary install failed/ln -s: exit status != 0\r\n"); + return false; + } + } catch (Exception e) { + installMessage.append("mosh binary install failed/ln -s: "+e.toString()+"\r\n"); + return false; + } + installMessage.append("mosh-client binary done\r\n"); + Editor edit = prefs.edit(); + edit.putString(PreferenceConstants.INSTALLED_MOSH_VERSION, BINARY_VERSION); + edit.commit(); + } + return true; + } + + private boolean use_pie() { // use binaries compiled with -fPIE + return Build.VERSION.SDK_INT >= 21; // Android Lollipop or later + } + + private boolean installBusybox() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String busyboxVersion = prefs.getString(PreferenceConstants.INSTALLED_MOSH_VERSION, ""); + + File busybox_path = new File(bindir, "busybox"); + if(!busybox_path.exists() || !busyboxVersion.equals(BINARY_VERSION)) { + installMessage.append("installing busybox binary\r\n"); + try { + InputStream busybox; + if(use_pie()) { + busybox = context.getResources().openRawResource(R.raw.busybox); + } else { + busybox = context.getResources().openRawResource(R.raw.busybox_arm_nopie); + } + FileOutputStream busybox_out = new FileOutputStream(busybox_path.getPath(), false); + byte[] buffer = new byte[4096]; + int num; + + while((num = busybox.read(buffer)) > 0) { + busybox_out.write(buffer,0,num); + } + busybox_out.close(); + busybox.close(); + } catch(IOException e) { + installMessage.append("mosh binary install failed/busybox: "+e.toString()+"\r\n"); + return false; + } + try { + File chmod_bin = new File("/system/bin/chmod"); + if(!chmod_bin.exists()) { + chmod_bin = new File("/system/xbin/chmod"); + if(!chmod_bin.exists()) { + installMessage.append("mosh binary install failed/chmod: unable to find chmod\r\n"); + return false; + } + } + Process process = Runtime.getRuntime().exec(chmod_bin.getPath()+" 755 "+busybox_path.getPath()); + if(process.waitFor() > 0) { + installMessage.append("mosh binary install failed/chmod: exit status != 0\r\n"); + return false; + } + } catch (Exception e) { + installMessage.append("mosh binary install failed/chmod: "+e.toString()+"\r\n"); + return false; + } + installMessage.append("busybox written\r\n"); + } + busybox_path.setExecutable(true); + + return true; + } +} diff --git a/app/src/main/java/org/connectbot/util/PreferenceConstants.java b/app/src/main/java/org/connectbot/util/PreferenceConstants.java index 0b1169e16..c33fc38e4 100644 --- a/app/src/main/java/org/connectbot/util/PreferenceConstants.java +++ b/app/src/main/java/org/connectbot/util/PreferenceConstants.java @@ -87,6 +87,8 @@ private PreferenceConstants() { public static final String NO = "no"; public static final String ALT = "alt"; + public static final String INSTALLED_MOSH_VERSION = "moshVersion"; + /* Backup identifiers */ public static final String BACKUP_PREF_KEY = "prefs"; } diff --git a/app/src/main/res/raw/busybox b/app/src/main/res/raw/busybox new file mode 100755 index 000000000..1ed4adaf6 Binary files /dev/null and b/app/src/main/res/raw/busybox differ diff --git a/app/src/main/res/raw/busybox_arm_nopie b/app/src/main/res/raw/busybox_arm_nopie new file mode 100755 index 000000000..ec3eef67a Binary files /dev/null and b/app/src/main/res/raw/busybox_arm_nopie differ diff --git a/app/src/main/res/raw/busybox_mips_pie b/app/src/main/res/raw/busybox_mips_pie new file mode 100755 index 000000000..201edb071 Binary files /dev/null and b/app/src/main/res/raw/busybox_mips_pie differ diff --git a/app/src/main/res/raw/busybox_x86_nopie b/app/src/main/res/raw/busybox_x86_nopie new file mode 100755 index 000000000..0519e941e Binary files /dev/null and b/app/src/main/res/raw/busybox_x86_nopie differ diff --git a/app/src/main/res/raw/busybox_x86_pie b/app/src/main/res/raw/busybox_x86_pie new file mode 100755 index 000000000..693123bb1 Binary files /dev/null and b/app/src/main/res/raw/busybox_x86_pie differ diff --git a/app/src/main/res/raw/mosh_tar_gz b/app/src/main/res/raw/mosh_tar_gz new file mode 100644 index 000000000..984ca3819 Binary files /dev/null and b/app/src/main/res/raw/mosh_tar_gz differ