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