Create only one SSH session per GitOperation (#1012)
This commit is contained in:
parent
d08397872a
commit
4e8da9b5f9
3 changed files with 35 additions and 61 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue