diff --git a/src/main/kotlin/app/credentials/CredentialsStateManager.kt b/src/main/kotlin/app/credentials/CredentialsStateManager.kt index f651962..61e733f 100644 --- a/src/main/kotlin/app/credentials/CredentialsStateManager.kt +++ b/src/main/kotlin/app/credentials/CredentialsStateManager.kt @@ -19,7 +19,11 @@ object CredentialsStateManager { sealed class CredentialsState { object None : CredentialsState() - object CredentialsRequested : CredentialsState() + sealed class CredentialsRequested : CredentialsState() + object SshCredentialsRequested : CredentialsRequested() + object HttpCredentialsRequested : CredentialsRequested() object CredentialsDenied : CredentialsState() - data class CredentialsAccepted(val user: String, val password: String) : CredentialsState() + sealed class CredentialsAccepted : CredentialsState() + data class SshCredentialsAccepted(val password: String) : CredentialsAccepted() + data class HttpCredentialsAccepted(val user: String, val password: String) : CredentialsAccepted() } \ No newline at end of file diff --git a/src/main/kotlin/app/credentials/GRemoteSession.kt b/src/main/kotlin/app/credentials/GRemoteSession.kt index 5f44950..b4c6957 100644 --- a/src/main/kotlin/app/credentials/GRemoteSession.kt +++ b/src/main/kotlin/app/credentials/GRemoteSession.kt @@ -1,32 +1,67 @@ package app.credentials +import org.apache.sshd.agent.SshAgent +import org.apache.sshd.agent.local.AgentImpl +import org.apache.sshd.agent.local.LocalAgentFactory import org.apache.sshd.client.SshClient +import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractive +import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory +import org.apache.sshd.client.auth.keyboard.UserInteraction +import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter +import org.apache.sshd.client.auth.password.UserAuthPassword import org.apache.sshd.client.future.ConnectFuture +import org.apache.sshd.client.session.ClientSession +import org.apache.sshd.common.NamedResource +import org.apache.sshd.common.config.keys.FilePasswordProvider +import org.apache.sshd.common.keyprovider.FileKeyPairProvider +import org.apache.sshd.common.session.SessionContext import org.eclipse.jgit.transport.RemoteSession import org.eclipse.jgit.transport.URIish +import java.lang.Exception +import java.security.KeyPair +import java.time.Duration import javax.inject.Inject import javax.inject.Provider + private const val DEFAULT_SSH_PORT = 22 class GRemoteSession @Inject constructor( private val processProvider: Provider, -): RemoteSession { +) : RemoteSession { + private val credentialsStateManager = CredentialsStateManager + private val client = SshClient.setUpDefaultClient() private var connectFuture: ConnectFuture? = null override fun exec(commandName: String, timeout: Int): Process { println(commandName) + val connectFuture = checkNotNull(connectFuture) + val session = connectFuture.clientSession - session.auth().verify() + + val auth = session.auth() + auth.addListener { arg0 -> + println("Authentication completed with " + if (arg0.isSuccess) "success" else "failure") + } + + session.waitFor( + listOf( + ClientSession.ClientSessionEvent.WAIT_AUTH, + ClientSession.ClientSessionEvent.CLOSED, + ClientSession.ClientSessionEvent.AUTHED + ), Duration.ofHours(2) + ) + auth.verify() val process = processProvider.get() process.setup(session, commandName) return process } + override fun disconnect() { client.close() } @@ -39,6 +74,25 @@ class GRemoteSession @Inject constructor( } else uri.port + val filePasswordProvider = object : FilePasswordProvider { + override fun getPassword(session: SessionContext?, resourceKey: NamedResource?, retryIndex: Int): String? { + credentialsStateManager.updateState(CredentialsState.SshCredentialsRequested) + + var credentials = credentialsStateManager.currentCredentialsState + while (credentials is CredentialsState.CredentialsRequested) { + // TODO check if support for ED25519 with pwd can be added + credentials = credentialsStateManager.currentCredentialsState + } + + return if(credentials !is CredentialsState.SshCredentialsAccepted) + null + else + credentials.password + } + } + + client.filePasswordProvider = filePasswordProvider + val connectFuture = client.connect(uri.user, uri.host, port) connectFuture.await() diff --git a/src/main/kotlin/app/credentials/HttpCredentialsProvider.kt b/src/main/kotlin/app/credentials/HttpCredentialsProvider.kt index 58bd23e..d53bf3d 100644 --- a/src/main/kotlin/app/credentials/HttpCredentialsProvider.kt +++ b/src/main/kotlin/app/credentials/HttpCredentialsProvider.kt @@ -23,15 +23,14 @@ class HttpCredentialsProvider : CredentialsProvider() { } override fun get(uri: URIish?, vararg items: CredentialItem?): Boolean { - credentialsStateManager.updateState(CredentialsState.CredentialsRequested) + credentialsStateManager.updateState(CredentialsState.HttpCredentialsRequested) - @Suppress("ControlFlowWithEmptyBody") var credentials = credentialsStateManager.currentCredentialsState while (credentials is CredentialsState.CredentialsRequested) { credentials = credentialsStateManager.currentCredentialsState } - if(credentials is CredentialsState.CredentialsAccepted) { + if(credentials is CredentialsState.HttpCredentialsAccepted) { val userItem = items.firstOrNull { it?.promptText == "Username" } val passwordItem = items.firstOrNull { it?.promptText == "Password" } diff --git a/src/main/kotlin/app/git/GitManager.kt b/src/main/kotlin/app/git/GitManager.kt index 5ae9291..6dfa145 100644 --- a/src/main/kotlin/app/git/GitManager.kt +++ b/src/main/kotlin/app/git/GitManager.kt @@ -226,8 +226,12 @@ class GitManager @Inject constructor( credentialsStateManager.updateState(CredentialsState.CredentialsDenied) } - fun credentialsAccepted(user: String, password: String) { - credentialsStateManager.updateState(CredentialsState.CredentialsAccepted(user, password)) + fun httpCredentialsAccepted(user: String, password: String) { + credentialsStateManager.updateState(CredentialsState.HttpCredentialsAccepted(user, password)) + } + + fun sshCredentialsAccepted(password: String) { + credentialsStateManager.updateState(CredentialsState.SshCredentialsAccepted(password)) } suspend fun diffListFromCommit(commit: RevCommit): List { @@ -284,6 +288,7 @@ class GitManager @Inject constructor( try { callback() } catch (ex: Exception) { + ex.printStackTrace() errorsManager.addError(newErrorNow(ex, ex.localizedMessage)) } finally { _processing.value = false diff --git a/src/main/kotlin/app/git/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt index 86eb62a..4f596b9 100644 --- a/src/main/kotlin/app/git/StatusManager.kt +++ b/src/main/kotlin/app/git/StatusManager.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.diff.DiffEntry +import org.eclipse.jgit.lib.Constants import javax.inject.Inject class StatusManager @Inject constructor() { @@ -68,6 +69,13 @@ class StatusManager @Inject constructor() { loadStatus(git) } +// suspend fun stageHunk(git: Git) { +//// val repository = git.repository +//// val objectInserter = repository.newObjectInserter() +// +//// objectInserter.insert(Constants.OBJ_BLOB,) +// } + suspend fun unstage(git: Git, diffEntry: DiffEntry) = withContext(Dispatchers.IO) { git.reset() .addPath(diffEntry.filePath) diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index e56c350..391224a 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -10,6 +10,7 @@ import app.credentials.CredentialsState import app.git.DiffEntryType import app.git.GitManager import app.ui.dialogs.NewBranchDialog +import app.ui.dialogs.PasswordDialog import app.ui.dialogs.UserPasswordDialog import openRepositoryDialog import org.eclipse.jgit.revwalk.RevCommit @@ -32,7 +33,7 @@ fun RepositoryOpenPage(gitManager: GitManager, dialogManager: DialogManager) { val credentialsState by gitManager.credentialsState.collectAsState() - if (credentialsState == CredentialsState.CredentialsRequested) { + if (credentialsState == CredentialsState.HttpCredentialsRequested) { dialogManager.show { UserPasswordDialog( onReject = { @@ -40,7 +41,20 @@ fun RepositoryOpenPage(gitManager: GitManager, dialogManager: DialogManager) { dialogManager.dismiss() }, onAccept = { user, password -> - gitManager.credentialsAccepted(user, password) + gitManager.httpCredentialsAccepted(user, password) + dialogManager.dismiss() + } + ) + } + } else if (credentialsState == CredentialsState.SshCredentialsRequested) { + dialogManager.show { + PasswordDialog( + onReject = { + gitManager.credentialsDenied() + dialogManager.dismiss() + }, + onAccept = { password -> + gitManager.sshCredentialsAccepted(password) dialogManager.dismiss() } ) diff --git a/src/main/kotlin/app/ui/dialogs/PasswordDialog.kt b/src/main/kotlin/app/ui/dialogs/PasswordDialog.kt new file mode 100644 index 0000000..1bcf55a --- /dev/null +++ b/src/main/kotlin/app/ui/dialogs/PasswordDialog.kt @@ -0,0 +1,83 @@ +package app.ui.dialogs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusOrder +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun PasswordDialog( + onReject: () -> Unit, + onAccept: (password: String) -> Unit +) { + var passwordField by remember { mutableStateOf("") } + val passwordFieldFocusRequester = remember { FocusRequester() } + val buttonFieldFocusRequester = remember { FocusRequester() } + + Column( + modifier = Modifier + .background(MaterialTheme.colors.background), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + + Text( + text = "Introduce your default SSH key's password", + modifier = Modifier + .padding(vertical = 8.dp), + ) + OutlinedTextField( + modifier = Modifier.padding(bottom = 8.dp) + .focusOrder(passwordFieldFocusRequester) { + this.next = buttonFieldFocusRequester + } + .width(300.dp), + value = passwordField, + singleLine = true, + label = { Text("Password", fontSize = 14.sp) }, + textStyle = TextStyle(fontSize = 14.sp), + onValueChange = { + passwordField = it + }, + visualTransformation = PasswordVisualTransformation() + ) + + Row( + modifier = Modifier + .padding(top = 16.dp) + .align(Alignment.End) + ) { + TextButton( + modifier = Modifier.padding(end = 8.dp), + onClick = { + onReject() + } + ) { + Text("Cancel") + } + Button( + modifier = Modifier.focusOrder(buttonFieldFocusRequester) { + this.previous = passwordFieldFocusRequester + }, + onClick = { + onAccept(passwordField) + } + ) { + Text("Ok") + } + } + + } + + LaunchedEffect(Unit) { + passwordFieldFocusRequester.requestFocus() + } +}