Create only one SSH session per GitOperation (#1012)

This commit is contained in:
Fabian Henneke 2020-08-12 16:41:11 +02:00 committed by GitHub
parent d08397872a
commit 4e8da9b5f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 35 additions and 61 deletions

View file

@ -44,8 +44,8 @@ class GitCommandExecutor(
length = Snackbar.LENGTH_INDEFINITE, length = Snackbar.LENGTH_INDEFINITE,
) )
var operationResult: Result = Result.Ok var operationResult: Result = Result.Ok
for (command in operation.commands) {
try { try {
for (command in operation.commands) {
when (command) { when (command) {
is CommitCommand -> { is CommitCommand -> {
// the previous status will eventually be used to avoid a commit // the previous status will eventually be used to avoid a commit
@ -108,10 +108,10 @@ class GitCommandExecutor(
} }
} }
} }
}
} catch (e: Exception) { } catch (e: Exception) {
operationResult = Result.Err(e) operationResult = Result.Err(e)
} }
}
when (operationResult) { when (operationResult) {
is Result.Err -> { is Result.Err -> {
activity.setResult(Activity.RESULT_CANCELED) activity.setResult(Activity.RESULT_CANCELED)
@ -131,7 +131,9 @@ class GitCommandExecutor(
} }
} }
snackbar.dismiss() snackbar.dismiss()
(SshSessionFactory.getInstance() as? SshjSessionFactory)?.clearCredentials() withContext(Dispatchers.IO) {
(SshSessionFactory.getInstance() as? SshjSessionFactory)?.close()
}
SshSessionFactory.setInstance(null) SshSessionFactory.setInstance(null)
} }

View file

@ -7,7 +7,6 @@ package com.zeapo.pwdstore.git.config
import android.util.Base64 import android.util.Base64
import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.w import com.github.ajalt.timberkt.w
import com.zeapo.pwdstore.utils.clear
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -36,60 +35,25 @@ import org.eclipse.jgit.transport.URIish
import org.eclipse.jgit.util.FS import org.eclipse.jgit.util.FS
sealed class SshAuthData { sealed class SshAuthData {
class Password(val passwordFinder: InteractivePasswordFinder) : SshAuthData() { class Password(val passwordFinder: InteractivePasswordFinder) : SshAuthData()
class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData()
override fun clearCredentials() {
passwordFinder.clearPasswords()
}
}
class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData() {
override fun clearCredentials() {
passphraseFinder.clearPasswords()
}
}
abstract fun clearCredentials()
} }
abstract class InteractivePasswordFinder : PasswordFinder { abstract class InteractivePasswordFinder : PasswordFinder {
private var isRetry = false
abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean)
private var isRetry = false
private var lastPassword: CharArray? = null
private val rememberToWipe: MutableList<CharArray> = mutableListOf()
fun resetForReuse() {
isRetry = false
}
fun clearPasswords() {
rememberToWipe.forEach { it.clear() }
lastPassword = null
}
final override fun reqPassword(resource: Resource<*>?): CharArray { final override fun reqPassword(resource: Resource<*>?): CharArray {
if (lastPassword != null && !isRetry) {
// This instance successfully authenticated in a previous authentication step and is
// now being reused for a new one. We try the previous password so that the user
// does not have to type it again.
isRetry = true
return lastPassword!!.clone().also { rememberToWipe.add(it) }
}
clearPasswords()
val password = runBlocking(Dispatchers.Main) { val password = runBlocking(Dispatchers.Main) {
suspendCoroutine<String?> { cont -> suspendCoroutine<String?> { cont ->
askForPassword(cont, isRetry) askForPassword(cont, isRetry)
} }
} }
isRetry = true isRetry = true
if (password == null) return password?.toCharArray()
throw SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER) ?: throw SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER)
val passwordChars = password.toCharArray().also { rememberToWipe.add(it) }
lastPassword = passwordChars
return passwordChars.clone().also { rememberToWipe.add(it) }
} }
final override fun shouldRetry(resource: Resource<*>?) = true final override fun shouldRetry(resource: Resource<*>?) = true
@ -97,12 +61,17 @@ abstract class InteractivePasswordFinder : PasswordFinder {
class SshjSessionFactory(private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() { class SshjSessionFactory(private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() {
private var currentSession: SshjSession? = null
override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession { override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession {
return SshjSession(uri, uri.user, authData, hostKeyFile).connect() return currentSession ?: SshjSession(uri, uri.user, authData, hostKeyFile).connect().also {
d { "New SSH connection created" }
currentSession = it
}
} }
fun clearCredentials() { fun close() {
authData.clearCredentials() currentSession?.close()
} }
} }
@ -155,11 +124,9 @@ private class SshjSession(uri: URIish, private val username: String, private val
when (authData) { when (authData) {
is SshAuthData.Password -> { is SshAuthData.Password -> {
ssh.authPassword(username, authData.passwordFinder) ssh.authPassword(username, authData.passwordFinder)
authData.passwordFinder.resetForReuse()
} }
is SshAuthData.PublicKeyFile -> { is SshAuthData.PublicKeyFile -> {
ssh.authPublickey(username, ssh.loadKeys(authData.keyFile.absolutePath, authData.passphraseFinder)) ssh.authPublickey(username, ssh.loadKeys(authData.keyFile.absolutePath, authData.passphraseFinder))
authData.passphraseFinder.resetForReuse()
} }
} }
return this return this
@ -167,17 +134,28 @@ private class SshjSession(uri: URIish, private val username: String, private val
override fun exec(commandName: String?, timeout: Int): Process { override fun exec(commandName: String?, timeout: Int): Process {
if (currentCommand != null) { if (currentCommand != null) {
w { "Killing old session" } w { "Killing old command" }
currentCommand?.close() disconnect()
currentCommand = null
} }
val session = ssh.startSession() val session = ssh.startSession()
currentCommand = session currentCommand = session
return SshjProcess(session.exec(commandName), timeout.toLong()) return SshjProcess(session.exec(commandName), timeout.toLong())
} }
/**
* Kills the current command if one is running and returns the session into a state where `exec`
* can be called.
*
* Note that this does *not* disconnect the session. Unfortunately, the function has to be
* called `disconnect` to override the corresponding abstract function in `RemoteSession`.
*/
override fun disconnect() { override fun disconnect() {
currentCommand?.close() currentCommand?.close()
currentCommand = null
}
fun close() {
disconnect()
ssh.close() ssh.close()
} }
} }

View file

@ -42,12 +42,6 @@ fun String.splitLines(): Array<String> {
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} }
fun CharArray.clear() {
forEachIndexed { i, _ ->
this[i] = 0.toChar()
}
}
val Context.clipboard get() = getSystemService<ClipboardManager>() val Context.clipboard get() = getSystemService<ClipboardManager>()
fun FragmentActivity.snackbar( fun FragmentActivity.snackbar(