From c4bdea1ae9b6ae2d77939bf0d185e0ab46bdbbfe Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sun, 5 May 2024 12:24:04 +0800 Subject: [PATCH] Add explicit TLS option for FTPSClient Fixes #3656 --- .../asynctasks/LoadFilesListTask.java | 1 + .../ftp/auth/FtpAuthenticationTask.kt | 2 + .../auth/FtpsAuthenticationTaskCallable.kt | 12 +- .../hostcert/FtpsGetHostCertificateTask.kt | 3 +- .../FtpsGetHostCertificateTaskCallable.kt | 12 +- .../filesystem/ftp/FTPClientImpl.kt | 7 +- .../ftp/NetCopyClientConnectionPool.kt | 20 +- .../filesystem/ftp/NetCopyClientUtils.kt | 12 +- .../filesystem/ftp/NetCopyConnectionInfo.kt | 3 +- .../ui/activities/MainActivity.java | 23 +- .../ui/dialogs/SftpConnectDialog.kt | 46 ++- .../amaze/filemanager/utils/smb/SmbUtil.kt | 4 + app/src/main/res/layout/sftp_dialog.xml | 11 +- app/src/main/res/values/strings.xml | 1 + .../ftp/NetCopyClientConnectionPoolTest.kt | 63 ++++ .../filesystem/ftp/NetCopyClientUtilTest.kt | 20 ++ .../dialogs/AbstractSftpConnectDialogTests.kt | 54 ++++ .../AbstractSftpConnectDialogUiTests.kt | 42 +++ ...SftpConnectDialogArgumentPopulationTest.kt | 275 ++++++++++++++++++ .../ui/dialogs/SftpConnectDialogFtpTest.kt | 159 ++++++++++ ...logTest.kt => SftpConnectDialogSshTest.kt} | 64 +--- .../amaze/filemanager/utils/SmbUtilTest.kt | 14 + 22 files changed, 773 insertions(+), 75 deletions(-) create mode 100644 app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolTest.kt create mode 100644 app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractSftpConnectDialogTests.kt create mode 100644 app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractSftpConnectDialogUiTests.kt create mode 100644 app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogArgumentPopulationTest.kt create mode 100644 app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogFtpTest.kt rename app/src/test/java/com/amaze/filemanager/ui/dialogs/{SftpConnectDialogTest.kt => SftpConnectDialogSshTest.kt} (81%) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java index 897b764102..2f1d29ef2f 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java @@ -161,6 +161,7 @@ public LoadFilesListTask( case SMB: list = listSmb(hFile, mainActivityViewModel, mainFragment); break; + case FTP: case SFTP: list = listSftp(mainActivityViewModel); break; diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTask.kt index 2ef16520b6..64983a3da5 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTask.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTask.kt @@ -38,6 +38,7 @@ class FtpAuthenticationTask( private val certInfo: JSONObject?, private val username: String, private val password: String?, + private val explicitTls: Boolean = false, ) : Task { override fun getTask(): FtpAuthenticationTaskCallable { return if (protocol == FTP_URI_PREFIX) { @@ -54,6 +55,7 @@ class FtpAuthenticationTask( certInfo!!, username, password ?: "", + explicitTls, ) } } diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpsAuthenticationTaskCallable.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpsAuthenticationTaskCallable.kt index b5c71cb54b..aa5e9b0d90 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpsAuthenticationTaskCallable.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpsAuthenticationTaskCallable.kt @@ -22,6 +22,8 @@ package com.amaze.filemanager.asynchronous.asynctasks.ftp.auth import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.ftp.FTPClientImpl +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.ARG_TLS +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.TLS_EXPLICIT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX import com.amaze.filemanager.utils.PasswordUtil @@ -39,6 +41,7 @@ class FtpsAuthenticationTaskCallable( private val certInfo: JSONObject, username: String, password: String, + private val explicitTls: Boolean, ) : FtpAuthenticationTaskCallable(hostname, port, username, password) { override fun call(): FTPClient { val ftpClient = createFTPClient() as FTPSClient @@ -71,8 +74,15 @@ class FtpsAuthenticationTaskCallable( @Suppress("LabeledExpression") override fun createFTPClient(): FTPClient { + val uri = + buildString { + append(FTPS_URI_PREFIX) + if (explicitTls) { + append("?$ARG_TLS=$TLS_EXPLICIT") + } + } return ( - NetCopyClientConnectionPool.ftpClientFactory.create(FTPS_URI_PREFIX) + NetCopyClientConnectionPool.ftpClientFactory.create(uri.toString()) as FTPSClient ).apply { this.hostnameVerifier = diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/hostcert/FtpsGetHostCertificateTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/hostcert/FtpsGetHostCertificateTask.kt index bba907687a..b4c507fd01 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/hostcert/FtpsGetHostCertificateTask.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/hostcert/FtpsGetHostCertificateTask.kt @@ -28,10 +28,11 @@ import java.lang.ref.WeakReference class FtpsGetHostCertificateTask( private val host: String, private val port: Int, + private val explicitTls: Boolean = false, context: Context, callback: (JSONObject) -> Unit, ) : AbstractGetHostInfoTask(host, port, callback) { val ctx: WeakReference = WeakReference(context) - override fun getTask(): FtpsGetHostCertificateTaskCallable = FtpsGetHostCertificateTaskCallable(host, port) + override fun getTask(): FtpsGetHostCertificateTaskCallable = FtpsGetHostCertificateTaskCallable(host, port, explicitTls) } diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/hostcert/FtpsGetHostCertificateTaskCallable.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/hostcert/FtpsGetHostCertificateTaskCallable.kt index a541636524..42373ce5cf 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/hostcert/FtpsGetHostCertificateTaskCallable.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/hostcert/FtpsGetHostCertificateTaskCallable.kt @@ -21,6 +21,8 @@ package com.amaze.filemanager.asynchronous.asynctasks.ftp.hostcert import androidx.annotation.WorkerThread +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.ARG_TLS +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.TLS_EXPLICIT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.CONNECT_TIMEOUT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX @@ -34,6 +36,7 @@ import javax.net.ssl.HostnameVerifier open class FtpsGetHostCertificateTaskCallable( private val hostname: String, private val port: Int, + private val explicitTls: Boolean = false, ) : Callable { @WorkerThread override fun call(): JSONObject? { @@ -57,5 +60,12 @@ open class FtpsGetHostCertificateTaskCallable( return result } - protected open fun createFTPClient(): FTPSClient = NetCopyClientConnectionPool.ftpClientFactory.create(FTPS_URI_PREFIX) as FTPSClient + protected open fun createFTPClient(): FTPSClient = + NetCopyClientConnectionPool.ftpClientFactory.create( + if (explicitTls) { + "$FTPS_URI_PREFIX?$ARG_TLS=$TLS_EXPLICIT" + } else { + FTPS_URI_PREFIX + }, + ) as FTPSClient } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/FTPClientImpl.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/FTPClientImpl.kt index 47065c3ff9..ece7f0d13a 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/FTPClientImpl.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/FTPClientImpl.kt @@ -34,8 +34,11 @@ class FTPClientImpl(private val ftpClient: FTPClient) : NetCopyClient @JvmStatic private val logger: Logger = LoggerFactory.getLogger(FTPClientImpl::class.java) - @JvmStatic - val ANONYMOUS = "anonymous" + const val ANONYMOUS = "anonymous" + + const val ARG_TLS = "tls" + + const val TLS_EXPLICIT = "explicit" private const val ALPHABET = "abcdefghijklmnopqrstuvwxyz1234567890" diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt index e4eecfdf30..91d43d52e4 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt @@ -25,7 +25,10 @@ import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.asynchronous.asynctasks.ftp.auth.FtpAuthenticationTask import com.amaze.filemanager.asynchronous.asynctasks.ssh.PemToKeyPairObservable import com.amaze.filemanager.asynchronous.asynctasks.ssh.SshAuthenticationTask +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.ARG_TLS +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.TLS_EXPLICIT import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils.extractBaseUriFrom +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.QUESTION_MARK import io.reactivex.Flowable import io.reactivex.Maybe import io.reactivex.Observable.create @@ -131,6 +134,7 @@ object NetCopyClientConnectionPool { username: String, password: String? = null, keyPair: KeyPair? = null, + explicitTls: Boolean = false, ): NetCopyClient<*>? { val url = NetCopyClientUtils.deriveUriFrom( @@ -140,6 +144,7 @@ object NetCopyClientConnectionPool { "", username, password, + explicitTls, ) var client = connections[url] if (client == null) { @@ -152,6 +157,7 @@ object NetCopyClientConnectionPool { username, password, keyPair, + explicitTls, ) if (client != null) connections[url] = client } else { @@ -182,7 +188,8 @@ object NetCopyClientConnectionPool { String, String?, KeyPair?, - ) -> NetCopyClient<*>? = { protocol, host, port, hostFingerprint, username, password, keyPair -> + Boolean, + ) -> NetCopyClient<*>? = { protocol, host, port, hostFingerprint, username, password, keyPair, explicitTls -> if (protocol == SSH_URI_PREFIX) { createSshClient(host, port, hostFingerprint!!, username, password, keyPair) } else { @@ -193,6 +200,7 @@ object NetCopyClientConnectionPool { hostFingerprint?.let { JSONObject(it) }, username, password, + explicitTls, ) } } @@ -354,6 +362,8 @@ object NetCopyClientConnectionPool { certInfo?.let { JSONObject(it) }, username, password, + true == arguments?.containsKey(ARG_TLS) && + TLS_EXPLICIT == arguments?.get(ARG_TLS), ) } } @@ -366,6 +376,7 @@ object NetCopyClientConnectionPool { certInfo: JSONObject?, username: String, password: String?, + explicitTls: Boolean = false, ): NetCopyClient? { val task = FtpAuthenticationTask( @@ -375,6 +386,7 @@ object NetCopyClientConnectionPool { certInfo, username, password, + explicitTls, ) val latch = CountDownLatch(1) var result: FTPClient? = null @@ -445,7 +457,11 @@ object NetCopyClientConnectionPool { override fun create(uri: String): FTPClient { return ( if (uri.startsWith(FTPS_URI_PREFIX)) { - FTPSClient("TLS", true) + FTPSClient( + "TLS", + !uri.contains(QUESTION_MARK) || + !uri.substringAfter(QUESTION_MARK).contains("$ARG_TLS=$TLS_EXPLICIT"), + ) } else { FTPClient() } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt index 92efb2bd12..557efb985a 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt @@ -26,6 +26,8 @@ import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.fileoperations.filesystem.DOESNT_EXIST import com.amaze.filemanager.fileoperations.filesystem.FolderState import com.amaze.filemanager.fileoperations.filesystem.WRITABLE_ON_REMOTE +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.ARG_TLS +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.TLS_EXPLICIT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_DEFAULT_PORT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_DEFAULT_PORT @@ -33,8 +35,10 @@ import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_ import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_DEFAULT_PORT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.getConnection +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.AND import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.AT import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLON +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.QUESTION_MARK import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.SLASH import com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate @@ -177,6 +181,10 @@ object NetCopyClientUtils { if (it.port > 0) { append(COLON).append(it.port) } + if (!it.arguments.isNullOrEmpty()) { + append(QUESTION_MARK) + .append(it.arguments?.entries?.joinToString(AND.toString())) + } } } } @@ -230,11 +238,13 @@ object NetCopyClientUtils { defaultPath: String? = null, username: String, password: String? = null, + explicitTls: Boolean = false, edit: Boolean = false, ): String { // FIXME: should be caller's responsibility var pathSuffix = defaultPath if (pathSuffix == null) pathSuffix = SLASH.toString() + if (explicitTls) pathSuffix = "$pathSuffix?$ARG_TLS=$TLS_EXPLICIT" val thisPassword = if (password == "" || password == null) { "" @@ -245,7 +255,7 @@ object NetCopyClientUtils { password.urlEncoded() }}" } - return if (username == "" && (true == password?.isEmpty())) { + return if (username == "") { "$prefix$hostname:$port$pathSuffix" } else { "$prefix$username$thisPassword@$hostname:$port$pathSuffix" diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt index 23fafb1f4a..1b7bc4a134 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt @@ -70,6 +70,7 @@ class NetCopyConnectionInfo(url: String) { const val AT = '@' const val SLASH = '/' const val COLON = ':' + const val QUESTION_MARK = '?' } init { @@ -157,7 +158,7 @@ class NetCopyConnectionInfo(url: String) { } override fun toString(): String { - return if (username.isNotEmpty()) { + return if (username.isNotBlank() && username.isNotEmpty()) { "$prefix$username@$host${if (port == 0) "" else ":$port"}${defaultPath ?: ""}" } else { "$prefix$host${if (port == 0) "" else ":$port"}${defaultPath ?: ""}" diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java index 691c8aa341..fe7dec1436 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java @@ -38,6 +38,11 @@ import static com.amaze.filemanager.fileoperations.filesystem.OperationTypeKt.RENAME; import static com.amaze.filemanager.fileoperations.filesystem.OperationTypeKt.SAVE_FILE; import static com.amaze.filemanager.fileoperations.filesystem.OperationTypeKt.UNDEFINED; +import static com.amaze.filemanager.filesystem.ftp.FTPClientImpl.ARG_TLS; +import static com.amaze.filemanager.filesystem.ftp.FTPClientImpl.TLS_EXPLICIT; +import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX; +import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX; +import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX; import static com.amaze.filemanager.ui.dialogs.SftpConnectDialog.ARG_ADDRESS; import static com.amaze.filemanager.ui.dialogs.SftpConnectDialog.ARG_DEFAULT_PATH; import static com.amaze.filemanager.ui.dialogs.SftpConnectDialog.ARG_EDIT; @@ -2114,16 +2119,28 @@ public void showSftpDialog(String name, String path, boolean edit) { (Function1) s -> GenericExtKt.urlDecoded(s, Charsets.UTF_8))); } - retval.putString(ARG_USERNAME, connectionInfo.getUsername()); + if (!TextUtils.isEmpty(connectionInfo.getUsername())) { + retval.putString(ARG_USERNAME, connectionInfo.getUsername()); + } if (connectionInfo.getPassword() == null) { retval.putBoolean(ARG_HAS_PASSWORD, false); - retval.putString(ARG_KEYPAIR_NAME, utilsHandler.getSshAuthPrivateKeyName(path)); + if (SSH_URI_PREFIX.equals(connectionInfo.getPrefix())) { + retval.putString(ARG_KEYPAIR_NAME, utilsHandler.getSshAuthPrivateKeyName(path)); + } } else { retval.putBoolean(ARG_HAS_PASSWORD, true); retval.putString(ARG_PASSWORD, connectionInfo.getPassword()); } retval.putBoolean(ARG_EDIT, edit); + + if ((FTP_URI_PREFIX.equals(connectionInfo.getPrefix()) + || FTPS_URI_PREFIX.equals(connectionInfo.getPrefix())) + && connectionInfo.getArguments() != null + && TLS_EXPLICIT.equals(connectionInfo.getArguments().get(ARG_TLS))) { + retval.putString(ARG_TLS, TLS_EXPLICIT); + } + return Flowable.just(retval); }) .subscribeOn(Schedulers.computation()) @@ -2131,7 +2148,7 @@ public void showSftpDialog(String name, String path, boolean edit) { bundle -> { sftpConnectDialog.setArguments(bundle); sftpConnectDialog.setCancelable(true); - sftpConnectDialog.show(getSupportFragmentManager(), "sftpdialog"); + sftpConnectDialog.show(getSupportFragmentManager(), SftpConnectDialog.TAG); }); } diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt index 828b108116..bf3c836077 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt @@ -37,6 +37,7 @@ import android.widget.AdapterView import android.widget.ArrayAdapter import androidx.activity.result.contract.ActivityResultContracts import androidx.core.text.isDigitsOnly +import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog @@ -51,8 +52,12 @@ import com.amaze.filemanager.database.UtilsHandler import com.amaze.filemanager.database.models.OperationData import com.amaze.filemanager.databinding.SftpDialogBinding import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.ARG_TLS +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.TLS_EXPLICIT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_DEFAULT_PORT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_DEFAULT_PORT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_DEFAULT_PORT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX @@ -90,6 +95,8 @@ class SftpConnectDialog : DialogFragment() { @JvmStatic private val log: Logger = LoggerFactory.getLogger(SftpConnectDialog::class.java) + const val TAG = "sftpdialog" + const val ARG_NAME = "name" const val ARG_EDIT = "edit" const val ARG_ADDRESS = "address" @@ -152,7 +159,7 @@ class SftpConnectDialog : DialogFragment() { .title(R.string.scp_connection) .autoDismiss(false) .customView(binding.root, true) - .theme(utilsProvider.appTheme.getMaterialDialogTheme()) + .theme(utilsProvider.appTheme.materialDialogTheme) .negativeText(R.string.cancel) .positiveText(if (edit) R.string.update else R.string.create) .positiveColor(accentColor) @@ -212,7 +219,7 @@ class SftpConnectDialog : DialogFragment() { // Otherwise, use given Bundle instance for filling in the blanks if (!edit) { connectionET.setText(R.string.scp_connection) - portET.setText(NetCopyClientConnectionPool.SSH_DEFAULT_PORT.toString()) + portET.setText(SSH_DEFAULT_PORT.toString()) protocolDropDown.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected( @@ -223,9 +230,9 @@ class SftpConnectDialog : DialogFragment() { ) { portET.setText( when (position) { - 1 -> NetCopyClientConnectionPool.FTP_DEFAULT_PORT.toString() - 2 -> NetCopyClientConnectionPool.FTPS_DEFAULT_PORT.toString() - else -> NetCopyClientConnectionPool.SSH_DEFAULT_PORT.toString() + 1 -> FTP_DEFAULT_PORT.toString() + 2 -> FTPS_DEFAULT_PORT.toString() + else -> SSH_DEFAULT_PORT.toString() }, ) chkFtpAnonymous.visibility = @@ -233,8 +240,14 @@ class SftpConnectDialog : DialogFragment() { 0 -> View.GONE else -> View.VISIBLE } + chkFtpExplicitTls.visibility = + when (position) { + 0 -> View.GONE + else -> View.VISIBLE + } if (position == 0) { chkFtpAnonymous.isChecked = false + chkFtpExplicitTls.isChecked = false } selectPemBTN.visibility = when (position) { @@ -257,21 +270,28 @@ class SftpConnectDialog : DialogFragment() { ipET.setText(requireArguments().getString(ARG_ADDRESS)) portET.setText(requireArguments().getInt(ARG_PORT).toString()) defaultPathET.setText(requireArguments().getString(ARG_DEFAULT_PATH)) - usernameET.setText(requireArguments().getString(ARG_USERNAME)) + usernameET.setText(requireArguments().getString(ARG_USERNAME) ?: "") + if ("" == (requireArguments().getString(ARG_USERNAME) ?: "")) { + chkFtpAnonymous.isChecked = true + } if (requireArguments().getBoolean(ARG_HAS_PASSWORD)) { passwordET.setHint(R.string.password_unchanged) } else { selectedParsedKeyPairName = requireArguments().getString(ARG_KEYPAIR_NAME) selectPemBTN.text = selectedParsedKeyPairName } + if (TLS_EXPLICIT == requireArguments().getString(ARG_TLS)) { + chkFtpExplicitTls.isChecked = true + } oldPath = NetCopyClientUtils.deriveUriFrom( requireArguments().getString(ARG_PROTOCOL)!!, requireArguments().getString(ARG_ADDRESS)!!, requireArguments().getInt(ARG_PORT), requireArguments().getString(ARG_DEFAULT_PATH, ""), - requireArguments().getString(ARG_USERNAME)!!, + requireArguments().getString(ARG_USERNAME) ?: "", requireArguments().getString(ARG_PASSWORD), + TLS_EXPLICIT == requireArguments().getString(ARG_TLS), edit, ) } @@ -470,13 +490,14 @@ class SftpConnectDialog : DialogFragment() { FtpsGetHostCertificateTask( hostname, port, + explicitTls, requireContext(), ) { hostInfo -> createFirstConnectCallback.invoke( edit, this, StringBuilder(hostname).also { - if (port != NetCopyClientConnectionPool.FTPS_DEFAULT_PORT && port > 0) { + if (port != FTPS_DEFAULT_PORT && port > 0) { it.append(':').append(port) } }.toString(), @@ -578,6 +599,7 @@ class SftpConnectDialog : DialogFragment() { FtpsGetHostCertificateTask( hostname, port, + explicitTls, requireContext(), ) { hostInfo: JSONObject -> createReconnectSecureServerCallback( @@ -695,6 +717,7 @@ class SftpConnectDialog : DialogFragment() { password }, selectedParsedKeyPair, + explicitTls, )?.run { if (DataUtils.getInstance().containsServer(encryptedPath) == -1) { DataUtils.getInstance().addServer(arrayOf(connectionName, encryptedPath)) @@ -784,7 +807,7 @@ class SftpConnectDialog : DialogFragment() { } } - private data class ConnectionSettings( + internal data class ConnectionSettings( val prefix: String, val connectionName: String, val hostname: String, @@ -794,6 +817,7 @@ class SftpConnectDialog : DialogFragment() { val password: String? = null, val selectedParsedKeyPairName: String? = null, val selectedParsedKeyPair: KeyPair? = null, + val explicitTls: Boolean = false, ) { fun toUriString() = NetCopyClientUtils.deriveUriFrom( @@ -803,6 +827,7 @@ class SftpConnectDialog : DialogFragment() { defaultPath, username, password, + explicitTls, ) } @@ -836,5 +861,8 @@ class SftpConnectDialog : DialogFragment() { }, selectedParsedKeyPairName = this.selectedParsedKeyPairName, selectedParsedKeyPair = selectedParsedKeyPair, + explicitTls = + binding.chkFtpExplicitTls.isVisible && + binding.chkFtpExplicitTls.isChecked, ) } diff --git a/app/src/main/java/com/amaze/filemanager/utils/smb/SmbUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/SmbUtil.kt index b56872cf84..b7aa0ff9d3 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/smb/SmbUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/SmbUtil.kt @@ -29,6 +29,7 @@ import com.amaze.filemanager.fileoperations.filesystem.WRITABLE_ON_REMOTE import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.AT import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLON +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.QUESTION_MARK import com.amaze.filemanager.filesystem.smb.CifsContexts.createWithDisableIpcSigningCheck import com.amaze.filemanager.utils.PasswordUtil import com.amaze.filemanager.utils.urlDecoded @@ -100,6 +101,9 @@ object SmbUtil { connectionInfo.defaultPath?.apply { buffer.append(this) } + if (path.contains(QUESTION_MARK)) { + buffer.append(QUESTION_MARK).append(path.substringAfter(QUESTION_MARK)) + } } return buffer.toString().replace("\n", "") } diff --git a/app/src/main/res/layout/sftp_dialog.xml b/app/src/main/res/layout/sftp_dialog.xml index f26dcc95f2..3f0c8a4e74 100644 --- a/app/src/main/res/layout/sftp_dialog.xml +++ b/app/src/main/res/layout/sftp_dialog.xml @@ -134,11 +134,20 @@ app:layout_constraintTop_toBottomOf="@id/passwordTIL" android:layout_gravity="center_horizontal"/> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e2478e10d7..7d3f97ae2a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -593,6 +593,7 @@ SCP/SFTP Connection FTP Connection FTP over SSL Connection + Explicit TLS Port The authenticity of host \'%1$s\' can\'t be established.\n\n%2$s key fingerprint is %3$s.\n\nPlease tap \"Yes\" to confirm identity; otherwise, please tap \"No\". Verify Host diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolTest.kt new file mode 100644 index 0000000000..8fa61ae80b --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolTest.kt @@ -0,0 +1,63 @@ +package com.amaze.filemanager.filesystem.ftp + +import org.apache.commons.net.ftp.FTPSClient +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Unit tests for [NetCopyClientConnectionPool] + */ +class NetCopyClientConnectionPoolTest { + /** + * Test DefaultFTPClientFactory default behaviour with FTPClient + */ + @Test + fun `DefaultFTPClientFactory default behaviour with FTPClient`() { + val factory = NetCopyClientConnectionPool.DefaultFTPClientFactory() + val result = factory.create("ftp://127.0.0.1:2121") + assertFalse(result is FTPSClient) + } + + /** + * Test DefaultFTPClientFactory default behaviour with FTPSClient + */ + @Test + fun `DefaultFTPClientFactory default behaviour with FTPSClient`() { + val factory = NetCopyClientConnectionPool.DefaultFTPClientFactory() + val result = factory.create("ftps://127.0.0.1:2121") + assertTrue(result is FTPSClient) + val isImplicit = FTPSClient::class.java.getDeclaredField("isImplicit") + isImplicit.isAccessible = true + assertTrue(isImplicit.get(result) as Boolean) + } + + /** + * Test DefaultFTPClientFactory with URI having tls != explicit + */ + @Test + fun `DefaultFTPClientFactory with URI having tls != explicit`() { + val factory = NetCopyClientConnectionPool.DefaultFTPClientFactory() + var result = factory.create("ftps://127.0.0.1:2121?tls=implicit") + assertTrue(result is FTPSClient) + val isImplicit = FTPSClient::class.java.getDeclaredField("isImplicit") + isImplicit.isAccessible = true + assertTrue(isImplicit.get(result) as Boolean) + result = factory.create("ftps://127.0.0.1:2121?explicitTls=true") + assertTrue(result is FTPSClient) + assertTrue(isImplicit.get(result) as Boolean) + } + + /** + * Test DefaultFTPClientDirectory with URI having tls=explicit + */ + @Test + fun `DefaultFTPClientDirectory with URI having tls=explicit`() { + val factory = NetCopyClientConnectionPool.DefaultFTPClientFactory() + val result = factory.create("ftps://127.0.0.1:2121?tls=explicit") + assertTrue(result is FTPSClient) + val isImplicit = FTPSClient::class.java.getDeclaredField("isImplicit") + isImplicit.isAccessible = true + assertFalse(isImplicit.get(result) as Boolean) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt index f4033b4bc8..5ae3563d59 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt @@ -140,5 +140,25 @@ class NetCopyClientUtilTest { "ftp://127.0.0.1:2211", NetCopyClientUtils.extractBaseUriFrom("ftp://127.0.0.1:2211/pub/notice.txt"), ) + assertEquals( + "ftps://127.0.0.1:21221?tls=explicit", + NetCopyClientUtils.extractBaseUriFrom("ftps://127.0.0.1:21221?tls=explicit"), + ) + assertEquals( + "ftps://127.0.0.1:21221?tls=explicit", + NetCopyClientUtils.extractBaseUriFrom("ftps://127.0.0.1:21221/tmp/pub?tls=explicit"), + ) + assertEquals( + "ftps://127.0.0.1:21221?tls=explicit", + NetCopyClientUtils.extractBaseUriFrom("ftps://127.0.0.1:21221/pub/Incoming/shared/test.txt?tls=explicit"), + ) + assertEquals( + "ssh://root@127.0.0.1:22222?foo=bar&timeout=3000&2fa=true", + NetCopyClientUtils.extractBaseUriFrom("ssh://root@127.0.0.1:22222?foo=bar&timeout=3000&2fa=true"), + ) + assertEquals( + "ssh://root@127.0.0.1:22222?foo=bar&timeout=3000&2fa=true", + NetCopyClientUtils.extractBaseUriFrom("ssh://root@127.0.0.1:22222/mnt/data?foo=bar&timeout=3000&2fa=true"), + ) } } diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractSftpConnectDialogTests.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractSftpConnectDialogTests.kt new file mode 100644 index 0000000000..c1181a6e61 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractSftpConnectDialogTests.kt @@ -0,0 +1,54 @@ +package com.amaze.filemanager.ui.dialogs + +import com.amaze.filemanager.ui.activities.AbstractMainActivityTestBase +import org.junit.After +import org.junit.Before +import org.mockito.MockedConstruction +import org.mockito.Mockito.mockConstruction +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.doCallRealMethod + +abstract class AbstractSftpConnectDialogTests : AbstractMainActivityTestBase() { + protected lateinit var mc: MockedConstruction + + /** + * Setups before test. + */ + @Before + override fun setUp() { + super.setUp() + mc = + mockConstruction( + SftpConnectDialog::class.java, + ) { mock: SftpConnectDialog, _: MockedConstruction.Context? -> + doCallRealMethod().`when`(mock).arguments = any() + `when`(mock.arguments).thenCallRealMethod() + } + } + + /** + * Post test cleanups. + */ + @After + override fun tearDown() { + super.tearDown() + mc.close() + } + + companion object { + @JvmStatic + protected val BUNDLE_KEYS = + arrayOf( + "address", + "port", + "keypairName", + "name", + "username", + "password", + "edit", + "defaultPath", + "tls", + ) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractSftpConnectDialogUiTests.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractSftpConnectDialogUiTests.kt new file mode 100644 index 0000000000..1f5d7629aa --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractSftpConnectDialogUiTests.kt @@ -0,0 +1,42 @@ +package com.amaze.filemanager.ui.dialogs + +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import com.afollestad.materialdialogs.MaterialDialog +import com.amaze.filemanager.ui.activities.AbstractMainActivityTestBase +import com.amaze.filemanager.ui.activities.MainActivity +import org.junit.Assert.assertTrue +import org.robolectric.shadows.ShadowDialog +import org.robolectric.shadows.ShadowLooper + +/** + * Base class for [SftpConnectDialog] UI level tests. + */ +abstract class AbstractSftpConnectDialogUiTests : AbstractMainActivityTestBase() { + /** + * Create and display [SftpConnectDialog] with Robolectric and AndroidX test. + * + * @param arguments [Bundle] of arguments + * @param withDialog Lambda performing test + */ + protected fun doTestWithDialog( + arguments: Bundle, + withDialog: (SftpConnectDialog, MaterialDialog) -> Unit, + ) { + val scenario = ActivityScenario.launch(MainActivity::class.java) + ShadowLooper.idleMainLooper() + scenario.moveToState(Lifecycle.State.STARTED) + scenario.onActivity { activity -> + SftpConnectDialog().run { + this.arguments = arguments + this.show(activity.supportFragmentManager, SftpConnectDialog.TAG) + ShadowLooper.runUiThreadTasks() + assertTrue(ShadowDialog.getLatestDialog().isShowing) + withDialog.invoke(this, ShadowDialog.getLatestDialog() as MaterialDialog) + } + scenario.moveToState(Lifecycle.State.DESTROYED) + scenario.close() + } + } +} diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogArgumentPopulationTest.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogArgumentPopulationTest.kt new file mode 100644 index 0000000000..46d3cb015b --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogArgumentPopulationTest.kt @@ -0,0 +1,275 @@ +package com.amaze.filemanager.ui.dialogs + +import android.os.Bundle +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.ARG_TLS +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_ADDRESS +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_DEFAULT_PATH +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_EDIT +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_HAS_PASSWORD +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_NAME +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_PASSWORD +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_PORT +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_PROTOCOL +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_USERNAME +import com.amaze.filemanager.utils.PasswordUtil +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Tests [SftpConnectDialog] populating arguments to UI at edit mode. + */ +class SftpConnectDialogArgumentPopulationTest : AbstractSftpConnectDialogUiTests() { + /** + * Test scenario with FTP, username and password + */ + @Test + fun testFtpWithUsernamePassword() { + doTestWithDialog( + arguments = + Bundle().also { + it.putString(ARG_PROTOCOL, FTP_URI_PREFIX) + it.putString(ARG_NAME, "FTP Connection") + it.putString(ARG_ADDRESS, "127.0.0.1") + it.putInt(ARG_PORT, 2121) + it.putString(ARG_USERNAME, "root") + it.putString( + ARG_PASSWORD, + PasswordUtil.encryptPassword( + AppConfig.getInstance(), + "abcdefgh", + ), + ) + it.putBoolean(ARG_HAS_PASSWORD, true) + it.putBoolean(ARG_EDIT, true) + }, + withDialog = { sftpConnectDialog, materialDialog -> + assertNotNull(sftpConnectDialog.binding) + requireNotNull(sftpConnectDialog.binding).let { binding -> + assertFalse(binding.chkFtpExplicitTls.isChecked) + assertFalse(binding.chkFtpAnonymous.isChecked) + assertEquals(2121, binding.portET.text.toString().toInt()) + assertEquals("root", binding.usernameET.text.toString()) + assertEquals("FTP Connection", binding.connectionET.text.toString()) + assertEquals(1, binding.protocolDropDown.selectedItemPosition) + assertEquals("", binding.passwordET.text.toString()) + } + }, + ) + } + + /** + * Test scenario with FTP, username and password and default path + */ + @Test + fun testFtpWithUsernamePasswordAndDefaultPath() { + doTestWithDialog( + arguments = + Bundle().also { + it.putString(ARG_PROTOCOL, FTP_URI_PREFIX) + it.putString(ARG_NAME, "FTP Connection") + it.putString(ARG_ADDRESS, "127.0.0.1") + it.putInt(ARG_PORT, 2121) + it.putString(ARG_USERNAME, "root") + it.putString(ARG_DEFAULT_PATH, "/root/Private") + it.putString( + ARG_PASSWORD, + PasswordUtil.encryptPassword( + AppConfig.getInstance(), + "abcdefgh", + ), + ) + it.putBoolean(ARG_HAS_PASSWORD, true) + it.putBoolean(ARG_EDIT, true) + }, + withDialog = { sftpConnectDialog, materialDialog -> + assertNotNull(sftpConnectDialog.binding) + requireNotNull(sftpConnectDialog.binding).let { binding -> + assertFalse(binding.chkFtpExplicitTls.isChecked) + assertFalse(binding.chkFtpAnonymous.isChecked) + assertEquals(2121, binding.portET.text.toString().toInt()) + assertEquals("root", binding.usernameET.text.toString()) + assertEquals("FTP Connection", binding.connectionET.text.toString()) + assertEquals(1, binding.protocolDropDown.selectedItemPosition) + assertEquals("", binding.passwordET.text.toString()) + assertEquals("/root/Private", binding.defaultPathET.text.toString()) + } + }, + ) + } + + /** + * Test scenario with FTP, anonymous + */ + @Test + fun testFtpWithAnonymous() { + doTestWithDialog( + arguments = + Bundle().also { + it.putString(ARG_PROTOCOL, FTP_URI_PREFIX) + it.putString(ARG_NAME, "FTP Connection") + it.putString(ARG_ADDRESS, "127.0.0.1") + it.putInt(ARG_PORT, 2121) + it.putBoolean(ARG_HAS_PASSWORD, false) + it.putBoolean(ARG_EDIT, true) + }, + withDialog = { sftpConnectDialog, materialDialog -> + assertNotNull(sftpConnectDialog.binding) + requireNotNull(sftpConnectDialog.binding).let { binding -> + assertFalse(binding.chkFtpExplicitTls.isChecked) + assertTrue(binding.chkFtpAnonymous.isChecked) + assertEquals(2121, binding.portET.text.toString().toInt()) + assertEquals("FTP Connection", binding.connectionET.text.toString()) + assertEquals(1, binding.protocolDropDown.selectedItemPosition) + assertEquals("", binding.passwordET.text.toString()) + } + }, + ) + } + + /** + * Test scenario with FTP, username and password + explicit TLS option + */ + @Test + fun testFtpWithUsernamePasswordAndExplicitTls() { + doTestWithDialog( + arguments = + Bundle().also { + it.putString(ARG_PROTOCOL, FTP_URI_PREFIX) + it.putString(ARG_NAME, "FTP Connection") + it.putString(ARG_ADDRESS, "127.0.0.1") + it.putInt(ARG_PORT, 2121) + it.putString(ARG_USERNAME, "root") + it.putString( + ARG_PASSWORD, + PasswordUtil.encryptPassword( + AppConfig.getInstance(), + "abcdefgh", + ), + ) + it.putBoolean(ARG_HAS_PASSWORD, true) + it.putString(ARG_TLS, "explicit") + it.putBoolean(ARG_EDIT, true) + }, + withDialog = { sftpConnectDialog, materialDialog -> + assertNotNull(sftpConnectDialog.binding) + requireNotNull(sftpConnectDialog.binding).let { binding -> + assertTrue(binding.chkFtpExplicitTls.isChecked) + assertFalse(binding.chkFtpAnonymous.isChecked) + assertEquals(2121, binding.portET.text.toString().toInt()) + assertEquals("root", binding.usernameET.text.toString()) + assertEquals("FTP Connection", binding.connectionET.text.toString()) + assertEquals(1, binding.protocolDropDown.selectedItemPosition) + assertEquals("", binding.passwordET.text.toString()) + } + }, + ) + } + + /** + * Test scenario with FTP, username and password + explicit TLS option + */ + @Test + fun testFtpWithUsernamePasswordAndExplicitTlsPlusDefaultPath() { + doTestWithDialog( + arguments = + Bundle().also { + it.putString(ARG_PROTOCOL, FTP_URI_PREFIX) + it.putString(ARG_NAME, "FTP Connection") + it.putString(ARG_ADDRESS, "127.0.0.1") + it.putInt(ARG_PORT, 2121) + it.putString(ARG_USERNAME, "root") + it.putString( + ARG_PASSWORD, + PasswordUtil.encryptPassword( + AppConfig.getInstance(), + "abcdefgh", + ), + ) + it.putString(ARG_DEFAULT_PATH, "/root/Documents") + it.putBoolean(ARG_HAS_PASSWORD, true) + it.putString(ARG_TLS, "explicit") + it.putBoolean(ARG_EDIT, true) + }, + withDialog = { sftpConnectDialog, materialDialog -> + assertNotNull(sftpConnectDialog.binding) + requireNotNull(sftpConnectDialog.binding).let { binding -> + assertTrue(binding.chkFtpExplicitTls.isChecked) + assertFalse(binding.chkFtpAnonymous.isChecked) + assertEquals(2121, binding.portET.text.toString().toInt()) + assertEquals("root", binding.usernameET.text.toString()) + assertEquals("FTP Connection", binding.connectionET.text.toString()) + assertEquals(1, binding.protocolDropDown.selectedItemPosition) + assertEquals("", binding.passwordET.text.toString()) + assertEquals("/root/Documents", binding.defaultPathET.text.toString()) + } + }, + ) + } + + /** + * Test scenario with FTP, anonymous and explicit TLS option + */ + @Test + fun testFtpWithAnonymousWithExplicitTls() { + doTestWithDialog( + arguments = + Bundle().also { + it.putString(ARG_PROTOCOL, FTP_URI_PREFIX) + it.putString(ARG_NAME, "FTP Connection") + it.putString(ARG_ADDRESS, "127.0.0.1") + it.putInt(ARG_PORT, 2121) + it.putBoolean(ARG_HAS_PASSWORD, false) + it.putBoolean(ARG_EDIT, true) + it.putString(ARG_TLS, "explicit") + }, + withDialog = { sftpConnectDialog, materialDialog -> + assertNotNull(sftpConnectDialog.binding) + requireNotNull(sftpConnectDialog.binding).let { binding -> + assertTrue(binding.chkFtpExplicitTls.isChecked) + assertTrue(binding.chkFtpAnonymous.isChecked) + assertEquals(2121, binding.portET.text.toString().toInt()) + assertEquals("FTP Connection", binding.connectionET.text.toString()) + assertEquals(1, binding.protocolDropDown.selectedItemPosition) + assertEquals("", binding.passwordET.text.toString()) + } + }, + ) + } + + /** + * Test scenario with FTP, anonymous and explicit TLS option + */ + @Test + fun testFtpWithAnonymousWithExplicitTlsAndDefaultPath() { + doTestWithDialog( + arguments = + Bundle().also { + it.putString(ARG_PROTOCOL, FTP_URI_PREFIX) + it.putString(ARG_NAME, "FTP Connection") + it.putString(ARG_ADDRESS, "127.0.0.1") + it.putInt(ARG_PORT, 2121) + it.putBoolean(ARG_HAS_PASSWORD, false) + it.putBoolean(ARG_EDIT, true) + it.putString(ARG_TLS, "explicit") + it.putString(ARG_DEFAULT_PATH, "/Incoming") + }, + withDialog = { sftpConnectDialog, materialDialog -> + assertNotNull(sftpConnectDialog.binding) + requireNotNull(sftpConnectDialog.binding).let { binding -> + assertTrue(binding.chkFtpExplicitTls.isChecked) + assertTrue(binding.chkFtpAnonymous.isChecked) + assertEquals(2121, binding.portET.text.toString().toInt()) + assertEquals("FTP Connection", binding.connectionET.text.toString()) + assertEquals(1, binding.protocolDropDown.selectedItemPosition) + assertEquals("", binding.passwordET.text.toString()) + assertEquals("/Incoming", binding.defaultPathET.text.toString()) + } + }, + ) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogFtpTest.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogFtpTest.kt new file mode 100644 index 0000000000..b832a8cfc7 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogFtpTest.kt @@ -0,0 +1,159 @@ +package com.amaze.filemanager.ui.dialogs + +import android.os.Bundle +import android.util.Base64 +import androidx.test.core.app.ApplicationProvider +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.ARG_TLS +import com.amaze.filemanager.filesystem.ftp.FTPClientImpl.Companion.TLS_EXPLICIT +import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_PASSWORD +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_USERNAME +import com.amaze.filemanager.utils.PasswordUtil +import org.awaitility.Awaitility.await +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.doCallRealMethod +import java.io.IOException +import java.security.GeneralSecurityException +import java.util.concurrent.TimeUnit + +/** + * Test [SftpConnectDialog] with FTP(S) connections. + */ +class SftpConnectDialogFtpTest : AbstractSftpConnectDialogTests() { + /** + * Test invoke [SftpConnectDialog] with arguments username and password. + */ + @Test + fun testInvokeSftpConnectionDialog() { + val verify = Bundle() + verify.putString("address", "127.0.0.1") + verify.putInt("port", 2121) + verify.putString("name", "FTP Connection") + verify.putString("username", "root") + verify.putString("password", "abcdefgh") + verify.putBoolean("hasPassword", true) + verify.putBoolean("edit", true) + + testOpenSftpConnectDialog("ftp://root:abcdefgh@127.0.0.1:2121", verify) + } + + /** + * Test invoke [SftpConnectDialog] with arguments username and password and explicit TLS option + */ + @Test + fun testInvokeSftpConnectionDialogWithExplicitTlsFlagEnabled() { + val verify = Bundle() + verify.putString("address", "127.0.0.1") + verify.putInt("port", 2121) + verify.putString("name", "FTP Connection") + verify.putString("username", "root") + verify.putString("password", "abcdefgh") + verify.putBoolean("hasPassword", true) + verify.putString("tls", "explicit") + verify.putBoolean("edit", true) + + testOpenSftpConnectDialog( + "ftp://root:abcdefgh@127.0.0.1:2121?tls=explicit", + verify, + true, + ) + } + + /** + * Test invoke [SftpConnectDialog] without any arguments + */ + @Test + fun testInvokeSftpConnectionDialogWithoutUsernamePassword() { + val verify = Bundle() + verify.putString("address", "127.0.0.1") + verify.putInt("port", 2121) + verify.putString("name", "FTP Connection") + verify.putBoolean("hasPassword", true) + verify.putBoolean("edit", true) + + testOpenSftpConnectDialog( + "ftp://127.0.0.1:2121", + verify, + false, + true, + ) + } + + /** + * Test invoke [SftpConnectDialog] without username/password but with explicit TLS option + */ + @Test + fun testInvokeSftpConnectionDialogWithoutUsernamePasswordAndExplicitTls() { + val verify = Bundle() + verify.putString("address", "127.0.0.1") + verify.putInt("port", 2121) + verify.putString("name", "FTP Connection") + verify.putBoolean("hasPassword", true) + verify.putString("tls", "explicit") + verify.putBoolean("edit", true) + + testOpenSftpConnectDialog( + "ftp://127.0.0.1:2121?tls=explicit", + verify, + true, + true, + ) + } + + @Throws(GeneralSecurityException::class, IOException::class) + private fun testOpenSftpConnectDialog( + uri: String, + verify: Bundle, + explicitTls: Boolean = false, + anonymous: Boolean = false, + ): SftpConnectDialog { + val activity = mock(MainActivity::class.java) + doCallRealMethod().`when`(activity).showSftpDialog( + any(), + any(), + anyBoolean(), + ) + activity.showSftpDialog( + "FTP Connection", + NetCopyClientUtils.encryptFtpPathAsNecessary(uri), + true, + ) + assertEquals(1, mc.constructed().size) + val mocked = mc.constructed()[0] + await().atMost(10, TimeUnit.SECONDS).until { mocked.arguments != null } + mocked.arguments?.let { args -> + if (explicitTls) { + assertTrue(args.containsKey(ARG_TLS)) + assertEquals(TLS_EXPLICIT, args.getString(ARG_TLS)) + } + val keys = BUNDLE_KEYS.clone().toMutableList() + if (anonymous) { + keys.remove(ARG_USERNAME) + keys.remove(ARG_PASSWORD) + } + for (key in keys) { + if (args.getString(key) != null) { + if (key == ARG_PASSWORD) { + assertEquals( + verify.getString(key), + PasswordUtil.decryptPassword( + ApplicationProvider.getApplicationContext(), + args.getString(key)!!, + Base64.URL_SAFE, + ), + ) + } else { + assertEquals(verify.getString(key), args.getString(key)) + } + } + } + } + return mocked + } +} diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogTest.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogSshTest.kt similarity index 81% rename from app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogTest.kt rename to app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogSshTest.kt index 2bde0226a7..5d574e67ed 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogSshTest.kt @@ -26,18 +26,14 @@ import androidx.test.core.app.ApplicationProvider import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.database.UtilsHandler import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils -import com.amaze.filemanager.ui.activities.AbstractMainActivityTestBase import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog.Companion.ARG_PASSWORD import com.amaze.filemanager.utils.PasswordUtil import org.awaitility.Awaitility.await -import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.MockedConstruction import org.mockito.Mockito.mock -import org.mockito.Mockito.mockConstruction import org.mockito.Mockito.`when` import org.mockito.kotlin.any import org.mockito.kotlin.doCallRealMethod @@ -46,34 +42,11 @@ import java.io.IOException import java.security.GeneralSecurityException import java.util.concurrent.TimeUnit +/** + * Test [SftpConnectDialog] with SSH connections. + */ @Suppress("StringLiteralDuplication") -class SftpConnectDialogTest : AbstractMainActivityTestBase() { - private lateinit var mc: MockedConstruction - - /** - * Setups before test. - */ - @Before - override fun setUp() { - super.setUp() - mc = - mockConstruction( - SftpConnectDialog::class.java, - ) { mock: SftpConnectDialog, _: MockedConstruction.Context? -> - doCallRealMethod().`when`(mock).arguments = any() - `when`(mock.arguments).thenCallRealMethod() - } - } - - /** - * Post test cleanups. - */ - @After - override fun tearDown() { - super.tearDown() - mc.close() - } - +class SftpConnectDialogSshTest : AbstractSftpConnectDialogTests() { /** * Test invoke [SftpConnectDialog] with arguments including keypair name. */ @@ -190,35 +163,20 @@ class SftpConnectDialogTest : AbstractMainActivityTestBase() { val mocked = mc.constructed()[0] await().atMost(10, TimeUnit.SECONDS).until { mocked.arguments != null } for (key in BUNDLE_KEYS) { - if (mocked.arguments!![key] != null) { - if (key != "password") { - assertEquals(verify[key], mocked.arguments!![key]) - } else { + if (mocked.arguments!!.getString(key) != null) { + if (key == ARG_PASSWORD) { assertEquals( - verify[key], + verify.getString(key), PasswordUtil.decryptPassword( ApplicationProvider.getApplicationContext(), - (mocked.arguments!![key] as String?)!!, + mocked.arguments!!.getString(key)!!, Base64.URL_SAFE, ), ) + } else { + assertEquals(verify.getString(key), mocked.arguments!!.getString(key)) } } } } - - companion object { - @JvmStatic - private val BUNDLE_KEYS = - arrayOf( - "address", - "port", - "keypairName", - "name", - "username", - "password", - "edit", - "defaultPath", - ) - } } diff --git a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt index ea1b3cb197..4cc95e2f00 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt @@ -78,6 +78,20 @@ class SmbUtilTest { assertEquals(path, decrypted) } + /** + * Test encrypt/decrypt FTP(S) URIs. + */ + @Test + fun testEncryptDecryptFtpsWithExtraParams() { + val path = "ftps://root:toor@127.0.0.1?tls=explicit" + val encrypted = getSmbEncryptedPath(ApplicationProvider.getApplicationContext(), path) + assertNotEquals(path, encrypted) + assertTrue(encrypted.startsWith("ftps://root:")) + assertTrue(encrypted.endsWith("@127.0.0.1?tls=explicit")) + val decrypted = getSmbDecryptedPath(ApplicationProvider.getApplicationContext(), encrypted) + assertEquals(path, decrypted) + } + /** * Test encrypt/decrypt URIs without username and password. It should stay the same. */