diff --git a/CHANGES.md b/CHANGES.md index a9250a543..a8bef9c2e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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. diff --git a/README.md b/README.md index 0b6305e18..21ef2aefa 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/android.md b/docs/android.md new file mode 100644 index 000000000..4cfc912aa --- /dev/null +++ b/docs/android.md @@ -0,0 +1,88 @@ +# 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(s). + +### 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 uses 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 `setUserHomeFolderResolver` 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 for standard O/S, but for Android one needs to call its `setUsersHomeDir` method **explicitly** - or extend it and override `getUserHomeDir` method. + +### 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)](./security-providers.md) + +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 SSHD 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() { + OsUtils.setAndroid(Boolean.TRUE); // if don't trust the automatic detection + System.setProperty("user.name", "....whatever..."); // just in case + OsUtils.setCurrentUser("....whatever..."); + } + + @Override + public void onCreate() { + super.onCreate(); + + // This is just an example - you are welcome to use whatever location you want + File filesDir = getFilesDir(); + Path filesPath = filesDir.toPath(); + System.setProperty("user.home", filesPath.toString()); // just in case + PathUtils.setUserHomeFolderResolver(() -> filesPath); + System.setProperty("user.dir", filesPath.toString()); // just in case + OsUtils.setCurrentWorkingDirectoryResolver(() -> filesPath); + + ...setup security provider(s)... + } +} +``` + +**Note:** these are the most *basic* settings - various extra adjustments might be required and they must be dealt with on a case-by-case manner. We do believe though that the code is flexible enough to provide the necessary solutions. As previously stated, even though we do not define Android as a goal, we would appreciate feedback on problems and solutions our users have encountered when running on Android (or iOs for that matter), and what suggestions they have for code changes that would facilitate this task - see [SSHD development mailing list](mailto:dev@mina.apache.org) or open an issue on this project - or better yet, a PR. + +## Further possible features + +Several Android related features come to mind as being useful, but as stated, due to severe lack of R&D resources (and not much demand from the community) we cannot devote the necessary effort to implement them. They are listed here in case they spark interest and someone undertakes their implementation (and hopefully contributes back via a PR) + +### [Uri](https://developer.android.com/reference/android/net/Uri?hl=en)-based [FileSystemProvider](https://developer.android.com/reference/java/nio/file/spi/FileSystemProvider) and [FileSystem](https://developer.android.com/reference/java/nio/file/FileSystem) + +The idea is to be able to wrap an [Uri](https://developer.android.com/reference/android/net/Uri?hl=en) that represents a files tree (e.g., obtained via [ACTION_OPEN_DOCUMENT_TREE](https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT_TREE)) into a [FileSystemProvider](https://developer.android.com/reference/java/nio/file/spi/FileSystemProvider) and [FileSystem](https://developer.android.com/reference/java/nio/file/FileSystem) so that it can be used to provide [Path](https://developer.android.com/reference/java/nio/file/Path.html)-like objects to SCP/SFTP client/server. The existing[DocumentFile](https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile) support object can be very helpful for this purpose. + +In this + +## Using [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences?hl=en) for global [configuration](https://github.com/apache/mina-sshd/blob/master/docs/internals.md#properties-and-inheritance-model) + +The SSHD code contains a robust and flexible mechanism for [configuring](https://github.com/apache/mina-sshd/blob/master/docs/internals.md#properties-and-inheritance-model) various properties used internally to control performance, memory and resources allocation, behavior, etc.. This mechanism relies heavily on Java properties which cannot be controlled when and Android application is launched. Instead, one could use the [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences?hl=en) to store the user's choices and then configure these properties when application starts. + diff --git a/docs/howto.md b/docs/howto.md index e9ea60b2f..70f0b64e4 100644 --- a/docs/howto.md +++ b/docs/howto.md @@ -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. \ No newline at end of file +A sample implementation can be found in the `EndlessTarpitSenderSupportDevelopment` class in the *sshd-contrib* package *test* section. diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java index e9e6a9cf3..fde0e8868 100644 --- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java +++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java @@ -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 diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java index ac8bf3f1f..15b296c96 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java @@ -260,6 +260,18 @@ public static SimpleImmutableEntry 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 perms = IoUtils.getPermissions(path, options); if (GenericUtils.isEmpty(perms)) { return null; diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/ExceptionUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/ExceptionUtils.java index 7a1158dcf..0e8016a0a 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/ExceptionUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/ExceptionUtils.java @@ -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); + } } } @@ -155,5 +160,4 @@ public static RuntimeException toRuntimeException(Throwable t, boolean peelThrow public static RuntimeException toRuntimeException(Throwable t) { return toRuntimeException(t, true); } - } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java index 54ca86b3d..eceac13e5 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java @@ -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. @@ -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 + * "android" (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 + * "dalvik" (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"; @@ -58,16 +79,100 @@ public final class OsUtils { public static final List WINDOWS_COMMAND = Collections.unmodifiableList(Collections.singletonList(WINDOWS_SHELL_COMMAND_NAME)); + /** + * System properties consulted in order to detect {@link #isAndroid() Android O/S}. + * + * @see Android Developer + */ + public static final List ANDROID_DETECTION_PROPERTIES + = Collections.unmodifiableList( + Arrays.asList( + "java.vendor", + "java.specification.vendor", + "java.vm.vendor", + "java.vm.specification.vendor")); + + public static final Predicate ANDROID_PROPERTY_VALUE_MATCHER + = v -> GenericUtils.trimToEmpty(v).toLowerCase().contains("android"); + + /** + * System properties consulted in order to detect {@link #isDalvikMachine() Dalvik machine}. + * + * @see Android Developer + */ + public static final List DALVIK_DETECTION_PROPERTIES + = Collections.unmodifiableList( + Arrays.asList( + "java.specification.name", + "java.vm.name", + "java.vm.specification.name")); + + public static final Predicate DALVIK_PROPERTY_VALUE_MATCHER + = v -> GenericUtils.trimToEmpty(v).toLowerCase().contains("dalvik"); + private static final AtomicReference CURRENT_USER_HOLDER = new AtomicReference<>(null); private static final AtomicReference JAVA_VERSION_HOLDER = new AtomicReference<>(null); private static final AtomicReference OS_TYPE_HOLDER = new AtomicReference<>(null); + private static final AtomicReference ANDROID_HOLDER = new AtomicReference<>(null); + private static final AtomicReference DALVIK_HOLDER = new AtomicReference<>(null); + + private static final AtomicReference> 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. Note: {@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. Note: {@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). Note: this does not preclude + * {@link #isAndroid()} or {@link #isDalvikMachine()} from being {@code true} as well. */ public static boolean isUNIX() { return !isWin32() && !isOSX(); @@ -103,6 +208,34 @@ public static void setOS(String os) { } } + private static boolean resolveAndroidSettingFlag( + AtomicReference flagHolder, String overrideProp, + Collection detectionProps, Predicate 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) */ @@ -142,6 +275,42 @@ public static List resolveDefaultInteractiveCommandElements(boolean winO } } + /** + * @return The (C)urrent (W)orking (D)irectory {@link Path} - {@code null} if cannot resolve it. Resolution occurs + * as follows: + *
    + *
  • Consult any currently registered {@link #setCurrentWorkingDirectoryResolver(Supplier) resolver}.
  • + * + *
  • If no resolver registered, then "user.dir" system property is consulted.
  • + *
+ * @see #setCurrentWorkingDirectoryResolver(Supplier) + */ + public static Path getCurrentWorkingDirectory() { + Supplier 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 "plug-in" 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 "user.dir" system property is consulted + */ + public static void setCurrentWorkingDirectoryResolver(Supplier cwdProvider) { + synchronized (CWD_PROVIDER_HOLDER) { + CWD_PROVIDER_HOLDER.set(cwdProvider); + } + } + /** * Get current user name * @@ -149,7 +318,7 @@ public static List resolveDefaultInteractiveCommandElements(boolean winO * @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 ? diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java index 4598cd697..8b2da5c0d 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java @@ -24,6 +24,8 @@ import java.nio.file.Paths; import java.util.Comparator; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; @@ -46,6 +48,10 @@ public final class PathUtils { public static final UnaryEquator EQ_CASE_SENSITIVE_FILENAME = (p1, p2) -> BY_CASE_SENSITIVE_FILENAME.compare(p1, p2) == 0; + public static final char HOME_TILDE_CHAR = '~'; + + private static final AtomicReference> USER_HOME_RESOLVER_HOLDER = new AtomicReference<>(); + private static final class LazyDefaultUserHomeFolderHolder { private static final Path PATH = Paths.get(ValidateUtils.checkNotNullAndNotEmpty(System.getProperty("user.home"), "No user home")) @@ -57,8 +63,6 @@ private LazyDefaultUserHomeFolderHolder() { } } - public static final char HOME_TILDE_CHAR = '~'; - /** * Private Constructor */ @@ -118,10 +122,28 @@ public static String normalizePath(String path) { /** * @return The {@link Path} to the currently running user home + * @see #setUserHomeFolderResolver(Supplier) */ @SuppressWarnings("synthetic-access") public static Path getUserHomeFolder() { - return LazyDefaultUserHomeFolderHolder.PATH; + Supplier resolver; + synchronized (USER_HOME_RESOLVER_HOLDER) { + resolver = USER_HOME_RESOLVER_HOLDER.get(); + } + + return (resolver == null) ? LazyDefaultUserHomeFolderHolder.PATH : resolver.get(); + } + + /** + * Set the reported value from {@link #getUserHomeFolder()} + * + * @param resolver The {@link Path} provider to report - if {@code null} then "user.home" system property + * will be used + */ + public static void setUserHomeFolderResolver(Supplier resolver) { + synchronized (USER_HOME_RESOLVER_HOLDER) { + USER_HOME_RESOLVER_HOLDER.set(resolver); + } } public static StringBuilder appendUserHome(StringBuilder sb) { diff --git a/sshd-common/src/test/java/org/apache/sshd/common/util/ExceptionUtilsAndroidPeelTest.java b/sshd-common/src/test/java/org/apache/sshd/common/util/ExceptionUtilsAndroidPeelTest.java new file mode 100644 index 000000000..28c3efb6a --- /dev/null +++ b/sshd-common/src/test/java/org/apache/sshd/common/util/ExceptionUtilsAndroidPeelTest.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sshd.common.util; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.management.ReflectionException; + +import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory; +import org.apache.sshd.util.test.JUnitTestSupport; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.Parameterized.UseParametersRunnerFactory; + +/** + * @author Apache MINA SSHD Project + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests +@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class) +@Category({ NoIoTestCase.class }) +public class ExceptionUtilsAndroidPeelTest extends JUnitTestSupport { + private final boolean androidMode; + + public ExceptionUtilsAndroidPeelTest(boolean androidMode) { + this.androidMode = androidMode; + } + + @Parameters(name = "android={0}") + public static List parameters() { + return Stream.of(Boolean.TRUE, Boolean.FALSE).map(v -> new Object[] { v }).collect(Collectors.toList()); + } + + @Test + public void testPeelJavaxManagementException() { + try { + OsUtils.setAndroid(androidMode); + + Exception original = new UnsupportedOperationException(getCurrentTestName() + "-wrapped"); + Throwable wrapper = new ReflectionException(original, original.getMessage() + "-wrapper"); + Throwable peeled = ExceptionUtils.peelException(wrapper); + if (androidMode) { + assertSame(wrapper, peeled); + } else { + assertSame(original, peeled); + } + } finally { + OsUtils.setAndroid(null); // restore auto-detection + } + } +} diff --git a/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/util/security/androidopenssl/AndroidOpenSSLSecurityProviderRegistrar.java b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/util/security/androidopenssl/AndroidOpenSSLSecurityProviderRegistrar.java index 5be511dae..97b60d5f8 100644 --- a/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/util/security/androidopenssl/AndroidOpenSSLSecurityProviderRegistrar.java +++ b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/util/security/androidopenssl/AndroidOpenSSLSecurityProviderRegistrar.java @@ -21,6 +21,7 @@ import java.security.Provider; import java.security.Security; +import org.apache.sshd.common.util.OsUtils; import org.apache.sshd.common.util.security.AbstractSecurityProviderRegistrar; /** @@ -42,7 +43,7 @@ public AndroidOpenSSLSecurityProviderRegistrar() { public boolean isSupported() { // Check that we are running on Android // https://developer.android.com/reference/java/lang/System#getProperties() - return "The Android Project".equals(System.getProperty("java.specification.vendor")); + return OsUtils.isAndroid(); } @Override diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpFileOpener.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpFileOpener.java index a49890c93..b9c637ac9 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpFileOpener.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpFileOpener.java @@ -20,6 +20,7 @@ package org.apache.sshd.scp.common; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -31,7 +32,6 @@ import java.nio.file.LinkOption; import java.nio.file.OpenOption; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; @@ -43,6 +43,7 @@ import org.apache.sshd.common.SshException; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.OsUtils; import org.apache.sshd.common.util.SelectorUtils; import org.apache.sshd.common.util.io.DirectoryScanner; import org.apache.sshd.common.util.io.IoUtils; @@ -132,8 +133,10 @@ default Iterable getMatchingFilesToSend(Session session, Path basedir, Str } if (basedir == null) { - String cwdLocal = System.getProperty("user.dir"); - Path cwdPath = Paths.get(cwdLocal); + Path cwdPath = OsUtils.getCurrentWorkingDirectory(); + if (cwdPath == null) { + throw new FileNotFoundException("No CWD value available"); + } basedir = cwdPath.toAbsolutePath(); }