Skip to content

Commit

Permalink
[apacheGH-322] Added basic Android awareness and hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Lyor Goldstein committed Mar 23, 2023
1 parent a87697b commit 6eab616
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* [GH-298](https://github.com/apache/mina-sshd/issues/298) Server side heartbeat not working.
* [GH-300](https://github.com/apache/mina-sshd/issues/300) Read the channel id in `SSH_MSG_CHANNEL_OPEN_CONFIRMATION` as unsigned int.
* [GH-313](https://github.com/apache/mina-sshd/issues/313) Log exceptions in the SFTP subsystem before sending a failure status reply.
* [GH-322](https://github.com/apache/mina-sshd/issues/322) Add basic Android O/S awareness.


* [SSHD-1295](https://issues.apache.org/jira/browse/SSHD-1295) Fix cancellation of futures and add options to cancel futures on time-outs.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,5 @@ mvn -Pquick clean install
## [TCP/IP Port Forwarding](./docs/technical/tcpip-forwarding.md)

## [Global Requests](./docs/technical/global_requests.md)

## [Android support](./docs/android.md)
66 changes: 66 additions & 0 deletions docs/android.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Android support

The SSHD team has not checked the compatibility and usability of the libraries for the Android O/S. Furthermore, at present it is not a stated goal of this project to actively support it, mainly because of the dire lack of available R&D resources and the relatively time consuming task of developing and testing code for Android. That being said, several "hooks" have been implemented aimed at facilitating the usage of these libraries on Android, though (as stated) no serious effort was made to thoroughly test them. The implemented support relies on feedback from users who have attempted this feat, the problems they discovered and how they tried (or failed) to overcome them.

## Specific issues

### O/S detection

[OsUtils](../sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java) has been enhanced to both automatically attempt to detect if currently runing in Android or being told so explicitly by the user - see `isAndroid/setAndroid` method.

### Accessing the current working directory

Instead of accessing the `user.dir` system property directly (which is missing in Android) [OsUtils](../sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java) has been enhanced to provide a `getCurrentWorkingDirectory` method - which by default still use the `user.dir` system property. However, the user can use `setCurrentWorkingDirectoryResolver` to reigster a callback that will return some user-controlled location instead. This is most important for [ScpFileOpener](../sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpFileOpener.java) `getMatchingFilesToSend` default implementation that uses the CWD as it base path if none provided by the caller.

### Detecting the user's home directory

Instead of accessing the `user.home` system property directly (which is missing in Android) [PathUtils](../sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java) now provides a `getUserHomeFolder` which by default still consults the `user.home` system property, unless the user has invoked `setUserHomeFolder` to provide a replacement for it.

Another aspect of this issue is the assignment of user "home" folder by a *server* that is running on Android. The [NativeFileSystemFactory](../sshd-common/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemFactory.java) auto-detects this folder of a standard O/S, but for Android one needs to call its `setUsersHomeDir` method **explicitly**.

### O/S dependenent code flow

There are a few locations where special consideration was made if the code detects that it is running on Android - these choices were made based on our current understanding of Android and are **independent** of the device's O/S API level. It is important to note that if API-level dependent flows are required, then much deeper change may be required. E.g. the [KeyUtils](../sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java) `validateStrictKeyFilePermissions` method returns an always valid result for Android.

### Security provider(s)

The SSHD code uses *Bouncycastle* if it detects it - however, on Android this can cause some issues - especially if the user's code also contains the BC libraries. It is not clear how to use it - especially since some articles suggest that BC is bundled into Android or has been so and now it is deprecated. Several [Stackoverflow](https://stackoverflow.com/) posts suggest that an **explicit** management is required - e.g.:

```java
import java.security.Security;

Security.removeProvider("BC" or "Bouncycastle");
Security.addProvider(new BouncycastleProvider());
```

The *sshd-contrib* module contains a [AndroidOpenSSLSecurityProviderRegistrar](../sshd-contrib/src/main/java/org/apache/sshd/contrib/common/util/security/androidopenssl/AndroidOpenSSLSecurityProviderRegistrar.java) class that can supposedly be used via the `SecurityUtils.registerSecurityProvider()` call. **Note:** we do not know for sure if this works for all/part of the needed security requirements since the code was donated without any in-depth explanation other than that "is works".

### Using [MINA](../sshd-mina) or [Netty](../sshd-netty) I/O factories

These factories have not been tested on Android and it is not clear if they work on it.

## Example code

The following is a simple/naive/simplistic code sample demonstrating the required initializations for an Android application using the SSDH code. Users are of course invited to make the necessary adjustments for their specific needs.

```java
import android.app.Application;

public class MyApplication extends Application {
public MyApplication() {
super();
}

@Override
public void onCreate() {
super.onCreate();

OsUtils.setAndroid(Boolean.TRUE); // if don't trust the automatic detection

File filesDir = getFilesDir();
Path filesPath = filesDir.toPath();
PathUtils.setUserHomeFolder(filesPath);
OsUtils.setCurrentWorkingDirectoryResolver(() -> filesPath);
}
}
```
2 changes: 1 addition & 1 deletion docs/howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ In order to achieve this one needs to use a `ReservedSessionMessagesHandler` on

The idea is to prevent the normal session establish flow by taking over the initial handshake identification and blocking the initial KEX message from the server.

A sample implementation can be found in the `EndlessTarpitSenderSupportDevelopment` class in the *sshd-contrib* package *test* section.
A sample implementation can be found in the `EndlessTarpitSenderSupportDevelopment` class in the *sshd-contrib* package *test* section.
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ public SftpCommandMain(SftpClient client) {
ValidateUtils.checkTrue(map.put(name, e) == null, "Multiple commands named '%s'", name);
}
commandsMap = Collections.unmodifiableMap(map);
cwdLocal = System.getProperty("user.dir");

Path cwdPath = OsUtils.getCurrentWorkingDirectory();
cwdLocal = Objects.toString(cwdPath, null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,18 @@ public static SimpleImmutableEntry<String, Object> validateStrictKeyFilePermissi
return null;
}

/*
* Android permission are not really consistent with standard Linux ones since the device is
* a "single" user O/S but with each application being a different "user". We therefore assume
* that if the application has access to a file, then it is good enough since there is really
* only one user, and we don't have to worry about multi-user access. Furthermore, the SE Linux
* security available on Android seems to be enough of a safeguard against inadvertent or even
* malicious access.
*/
if (OsUtils.isAndroid()) {
return null;
}

Collection<PosixFilePermission> perms = IoUtils.getPermissions(path, options);
if (GenericUtils.isEmpty(perms)) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,22 @@ public static Throwable peelException(Throwable t) {
if (target != null) {
return peelException(target);
}
} else if (t instanceof ReflectionException) {
Throwable target = ((ReflectionException) t).getTargetException();
if (target != null) {
return peelException(target);
}
} else if (t instanceof ExecutionException) {
return peelException(resolveExceptionCause(t));
} else if (t instanceof MBeanException) {
Throwable target = ((MBeanException) t).getTargetException();
if (target != null) {
return peelException(target);
}

// Android does not have these classes
if (!OsUtils.isAndroid()) {
if (t instanceof ReflectionException) {
Throwable target = ((ReflectionException) t).getTargetException();
if (target != null) {
return peelException(target);
}
} else if (t instanceof MBeanException) {
Throwable target = ((MBeanException) t).getTargetException();
if (target != null) {
return peelException(target);
}
}
}

Expand Down Expand Up @@ -155,5 +160,4 @@ public static RuntimeException toRuntimeException(Throwable t, boolean peelThrow
public static RuntimeException toRuntimeException(Throwable t) {
return toRuntimeException(t, true);
}

}
173 changes: 171 additions & 2 deletions sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@
*/
package org.apache.sshd.common.util;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
* Operating system dependent utility methods.
Expand All @@ -48,6 +53,22 @@ public final class OsUtils {
*/
public static final String OS_TYPE_OVERRIDE_PROP = "org.apache.sshd.osType";

/**
* Property that can be used to override the reported value from {@link #isAndroid()}. If not set then
* {@link #ANDROID_DETECTION_PROPERTIES} are used to determine its value. Otherwise, it must contain the string
* &quot;android&quot; (case-insensitive)
*
* @see #ANDROID_PROPERTY_VALUE_MATCHER
*/
public static final String ANDROID_MODE_OVERRIDE_PROP = "org.apache.sshd.androidMode";

/**
* Property that can be used to override the reported value from {@link #isDalvikMachine()}. If not set then
* {@link #DALVIK_DETECTION_PROPERTIES} are used to determine its value. Otherwise, it must contain the string
* &quot;dalvik&quot; (case-insensitive)
*/
public static final String DALVIK_MACHINE_OVERRIDE_PROP = "org.apache.sshd.dalvikMachine";

public static final String WINDOWS_SHELL_COMMAND_NAME = "cmd.exe";
public static final String LINUX_SHELL_COMMAND_NAME = "/bin/sh";

Expand All @@ -58,16 +79,100 @@ public final class OsUtils {
public static final List<String> WINDOWS_COMMAND
= Collections.unmodifiableList(Collections.singletonList(WINDOWS_SHELL_COMMAND_NAME));

/**
* System properties consulted in order to detect {@link #isAndroid() Android O/S}.
*
* @see <A HREF="https://developer.android.com/reference/java/lang/System#getProperties()">Android Developer</A>
*/
public static final List<String> ANDROID_DETECTION_PROPERTIES
= Collections.unmodifiableList(
Arrays.asList(
"java.vendor",
"java.specification.vendor",
"java.vm.vendor",
"java.vm.specification.vendor"));

public static final Predicate<String> ANDROID_PROPERTY_VALUE_MATCHER
= v -> GenericUtils.trimToEmpty(v).toLowerCase().contains("android");

/**
* System properties consulted in order to detect {@link #isDalvikMachine() Dalvik machine}.
*
* @see <A HREF="https://developer.android.com/reference/java/lang/System#getProperties()">Android Developer</A>
*/
public static final List<String> DALVIK_DETECTION_PROPERTIES
= Collections.unmodifiableList(
Arrays.asList(
"java.specification.name",
"java.vm.name",
"java.vm.specification.name"));

public static final Predicate<String> DALVIK_PROPERTY_VALUE_MATCHER
= v -> GenericUtils.trimToEmpty(v).toLowerCase().contains("dalvik");

private static final AtomicReference<String> CURRENT_USER_HOLDER = new AtomicReference<>(null);
private static final AtomicReference<VersionInfo> JAVA_VERSION_HOLDER = new AtomicReference<>(null);
private static final AtomicReference<String> OS_TYPE_HOLDER = new AtomicReference<>(null);

private static final AtomicReference<Boolean> ANDROID_HOLDER = new AtomicReference<>(null);
private static final AtomicReference<Boolean> DALVIK_HOLDER = new AtomicReference<>(null);

private static final AtomicReference<Supplier<? extends Path>> CWD_PROVIDER_HOLDER = new AtomicReference<>();

private OsUtils() {
throw new UnsupportedOperationException("No instance allowed");
}

/**
* @return true if the host is a UNIX system (and not Windows).
* @return {@code true} if currently running on Android. <U>Note:</U> {@link #isUNIX()} is also probably
* {@code true} as well, so special care must be taken in code that consults these values
* @see #ANDROID_DETECTION_PROPERTIES
* @see #ANDROID_MODE_OVERRIDE_PROP
* @see #ANDROID_PROPERTY_VALUE_MATCHER
*/
public static boolean isAndroid() {
return resolveAndroidSettingFlag(
ANDROID_HOLDER, ANDROID_MODE_OVERRIDE_PROP, ANDROID_DETECTION_PROPERTIES, ANDROID_PROPERTY_VALUE_MATCHER);
}

/**
* Override the value returned by {@link #isAndroid()} programmatically
*
* @param value Value to set if {@code null} then value is auto-detected
*/
public static void setAndroid(Boolean value) {
synchronized (ANDROID_HOLDER) {
ANDROID_HOLDER.set(value);
}
}

/**
* @return {@code true} if currently running on a Dalvik machine. <U>Note:</U> {@link #isUNIX()} and/or
* {@link #isAndroid()} are also probably {@code true} as well, so special care must be taken in code that
* consults these values
* @see #DALVIK_DETECTION_PROPERTIES
* @see #DALVIK_MACHINE_OVERRIDE_PROP
* @see #DALVIK_PROPERTY_VALUE_MATCHER
*/
public static boolean isDalvikMachine() {
return resolveAndroidSettingFlag(
DALVIK_HOLDER, DALVIK_MACHINE_OVERRIDE_PROP, DALVIK_DETECTION_PROPERTIES, DALVIK_PROPERTY_VALUE_MATCHER);
}

/**
* Override the value returned by {@link #isDalvikMachine()} programmatically
*
* @param value Value to set if {@code null} then value is auto-detected
*/
public static void setDalvikMachine(Boolean value) {
synchronized (DALVIK_HOLDER) {
DALVIK_HOLDER.set(value);
}
}

/**
* @return true if the host is a UNIX system (and not Windows). <U>Note:</U> this does <B>not</B> preclude
* {@link #isAndroid()} or {@link #isDalvikMachine()} from being {@code true} as well.
*/
public static boolean isUNIX() {
return !isWin32() && !isOSX();
Expand Down Expand Up @@ -103,6 +208,34 @@ public static void setOS(String os) {
}
}

private static boolean resolveAndroidSettingFlag(
AtomicReference<Boolean> flagHolder, String overrideProp,
Collection<String> detectionProps, Predicate<? super String> detector) {
synchronized (flagHolder) {
Boolean value = flagHolder.get();
if (value != null) {
return value.booleanValue();
}

String propValue = System.getProperty(overrideProp);
if (detector.test(propValue)) {
flagHolder.set(Boolean.TRUE);
return true;
}

for (String p : detectionProps) {
if (detector.test(propValue)) {
flagHolder.set(Boolean.TRUE);
return true;
}
}

flagHolder.set(Boolean.FALSE);
}

return false;
}

/**
* @return The resolved O/S type string if not already set (lowercase)
*/
Expand Down Expand Up @@ -142,14 +275,50 @@ public static List<String> resolveDefaultInteractiveCommandElements(boolean winO
}
}

/**
* @return The (C)urrent (W)orking (D)irectory {@link Path} - {@code null} if cannot resolve it. Resolution occurs
* as follows:
* <UL>
* <LI>Consult any currently registered {@link #setCurrentWorkingDirectoryResolver(Supplier) resolver}.</LI>
*
* <LI>If no resolver registered, then &quot;user.dir&quot; system property is consulted.</LI>
* </UL>
* @see #setCurrentWorkingDirectoryResolver(Supplier)
*/
public static Path getCurrentWorkingDirectory() {
Supplier<? extends Path> cwdProvider;
synchronized (CWD_PROVIDER_HOLDER) {
cwdProvider = CWD_PROVIDER_HOLDER.get();
}

if (cwdProvider != null) {
return cwdProvider.get();
}

String cwdLocal = System.getProperty("user.dir");
return GenericUtils.isBlank(cwdLocal) ? null : Paths.get(cwdLocal);
}

/**
* Allows the user to &quot;plug-in&quot; a resolver for the {@link #getCurrentWorkingDirectory()} method
*
* @param cwdProvider The {@link Supplier} of the (C)urrent (W)orking (D)irectory {@link Path} - if {@code null}
* then &quot;user.dir&quot; system property is consulted
*/
public static void setCurrentWorkingDirectoryResolver(Supplier<? extends Path> cwdProvider) {
synchronized (CWD_PROVIDER_HOLDER) {
CWD_PROVIDER_HOLDER.set(cwdProvider);
}
}

/**
* Get current user name
*
* @return Current user
* @see #CURRENT_USER_OVERRIDE_PROP
*/
public static String getCurrentUser() {
String username = null;
String username;
synchronized (CURRENT_USER_HOLDER) {
username = CURRENT_USER_HOLDER.get();
if (username != null) { // have we already resolved it ?
Expand Down
Loading

0 comments on commit 6eab616

Please sign in to comment.