diff options
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt')
| -rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt | 212 |
1 files changed, 212 insertions, 0 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt new file mode 100644 index 00000000..36dd95c4 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt @@ -0,0 +1,212 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git + +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.view.MenuItem +import androidx.annotation.CallSuper +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit +import androidx.core.text.isDigitsOnly +import androidx.preference.PreferenceManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zeapo.pwdstore.git.config.ConnectionMode +import com.zeapo.pwdstore.git.config.Protocol +import com.zeapo.pwdstore.git.config.SshApiSessionFactory +import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.getEncryptedPrefs +import java.io.File +import timber.log.Timber + +/** + * Abstract AppCompatActivity that holds some information that is commonly shared across git-related + * tasks and makes sense to be held here. + */ +abstract class BaseGitActivity : AppCompatActivity() { + lateinit var protocol: Protocol + lateinit var connectionMode: ConnectionMode + lateinit var url: String + lateinit var serverHostname: String + lateinit var serverPort: String + lateinit var serverUser: String + lateinit var serverPath: String + lateinit var username: String + lateinit var email: String + var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null + var identity: SshApiSessionFactory.ApiIdentity? = null + lateinit var settings: SharedPreferences + private set + private lateinit var encryptedSettings: SharedPreferences + + @CallSuper + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + settings = PreferenceManager.getDefaultSharedPreferences(this) + encryptedSettings = getEncryptedPrefs("git_operation") + protocol = Protocol.fromString(settings.getString("git_remote_protocol", null)) + connectionMode = ConnectionMode.fromString(settings.getString("git_remote_auth", null)) + serverHostname = settings.getString("git_remote_server", null) ?: "" + serverPort = settings.getString("git_remote_port", null) ?: "" + serverUser = settings.getString("git_remote_username", null) ?: "" + serverPath = settings.getString("git_remote_location", null) ?: "" + username = settings.getString("git_config_user_name", null) ?: "" + email = settings.getString("git_config_user_email", null) ?: "" + updateUrl() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + @CallSuper + override fun onDestroy() { + // Do not leak the service connection + if (identityBuilder != null) { + identityBuilder!!.close() + identityBuilder = null + } + super.onDestroy() + } + + /** + * Update the [url] field with the values that build it up. This function returns a boolean + * indicating whether or not the values are likely valid or not, and only adds the `origin` + * remote when it is. This check is not perfect, it is mostly meant to catch typos. + */ + fun updateUrl(): Boolean { + if (serverHostname.isEmpty() || !serverPort.isDigitsOnly()) + return false + + val previousUrl = if (::url.isInitialized) url else "" + val hostnamePart = serverHostname + val pathPart = if (serverPath.startsWith('/')) serverPath else "/$serverPath" + url = when (protocol) { + Protocol.Ssh -> { + val userPart = if (serverUser.isEmpty()) "" else "$serverUser@" + val portPart = + if (serverPort == "22" || serverPort.isEmpty()) "" else ":$serverPort" + // We have to specify the ssh scheme as this is the only way to pass a custom port. + "ssh://$userPart$hostnamePart$portPart$pathPart" + } + Protocol.Https -> { + val portPart = + if (serverPort == "443" || serverPort.isEmpty()) "" else ":$serverPort" + "https://$hostnamePart$portPart$pathPart" + } + } + if (PasswordRepository.isInitialized) + PasswordRepository.addRemote("origin", url, true) + // HTTPS authentication sends the password to the server, so we must wipe the password when + // the server is changed. + if (url != previousUrl && protocol == Protocol.Https) + encryptedSettings.edit { remove("https_password") } + return true + } + + /** + * Attempt to launch the requested Git operation. Depending on the configured auth, it may not + * be possible to launch the operation immediately. In that case, this function may launch an + * intermediate activity instead, which will gather necessary information and post it back via + * onActivityResult, which will then re-call this function. This may happen multiple times, + * until either an error is encountered or the operation is successfully launched. + * + * @param operation The type of git operation to launch + */ + fun launchGitOperation(operation: Int) { + val op: GitOperation + val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this)) + try { + // Before launching the operation with OpenKeychain auth, we need to issue several requests + // to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents, + // we just need to keep calling it until it returns a completed ApiIdentity. + if (connectionMode == ConnectionMode.OpenKeychain && identity == null) { + // Lazy initialization of the IdentityBuilder + if (identityBuilder == null) { + identityBuilder = SshApiSessionFactory.IdentityBuilder(this) + } + // Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure + // that onActivityResult is called with operation again, which will re-invoke us here + identity = identityBuilder!!.tryBuild(operation) + if (identity == null) + return + } + + op = when (operation) { + REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, this).setCommand(url) + REQUEST_PULL -> PullOperation(localDir, this).setCommand() + REQUEST_PUSH -> PushOperation(localDir, this).setCommand() + REQUEST_SYNC -> SyncOperation(localDir, this).setCommands() + BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this).setCommands() + REQUEST_RESET -> ResetToRemoteOperation(localDir, this).setCommands() + SshApiSessionFactory.POST_SIGNATURE -> return + else -> { + Timber.tag(TAG).e("Operation not recognized : $operation") + setResult(RESULT_CANCELED) + finish() + return + } + } + op.executeAfterAuthentication(connectionMode, serverUser, + File("$filesDir/.ssh_key"), identity) + } catch (e: Exception) { + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + } + + public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + // In addition to the pre-operation-launch series of intents for OpenKeychain auth + // that will pass through here and back to launchGitOperation, there is one + // synchronous operation that happens /after/ the operation has been launched in the + // background thread - the actual signing of the SSH challenge. We pass through the + // completed signature to the ApiIdentity, which will be blocked in the other thread + // waiting for it. + if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null) { + identity!!.postSignature(data) + + // If the signature failed (usually because it was cancelled), reset state + if (data == null) { + identity = null + identityBuilder = null + } + return + } + + if (resultCode == RESULT_CANCELED) { + setResult(RESULT_CANCELED) + finish() + } else if (resultCode == RESULT_OK) { + // If an operation has been re-queued via this mechanism, let the + // IdentityBuilder attempt to extract some updated state from the intent before + // trying to re-launch the operation. + if (identityBuilder != null) { + identityBuilder!!.consume(data) + } + launchGitOperation(requestCode) + } + super.onActivityResult(requestCode, resultCode, data) + } + + companion object { + const val REQUEST_ARG_OP = "OPERATION" + const val REQUEST_PULL = 101 + const val REQUEST_PUSH = 102 + const val REQUEST_CLONE = 103 + const val REQUEST_INIT = 104 + const val REQUEST_SYNC = 105 + const val BREAK_OUT_OF_DETACHED = 106 + const val REQUEST_RESET = 107 + const val TAG = "AbstractGitActivity" + } +} |
