From 7bdc2c4cf59333ebaab1236b4bf8fab3ef6483d2 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Sat, 8 Oct 2022 19:43:23 +0200 Subject: [PATCH] Added support for external git credentials helper [BETA] Fixes https://github.com/JetpackDuba/Gitnuro/issues/16 --- .../credentials/HttpCredentialsProvider.kt | 211 ++++++++++++++++-- .../di/factories/HttpCredentialsFactory.kt | 10 + .../gitnuro/exceptions/NotSupportedHelper.kt | 3 + .../CloneRepositoryUseCase.kt | 2 +- .../DeleteRemoteBranchUseCase.kt | 2 +- .../FetchAllBranchesUseCase.kt | 2 +- .../HandleTransportUseCase.kt | 8 +- .../remote_operations/PullBranchUseCase.kt | 2 +- .../PullFromSpecificBranchUseCase.kt | 2 +- .../remote_operations/PushBranchUseCase.kt | 2 +- .../PushToSpecificBranchUseCase.kt | 2 +- 11 files changed, 219 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/di/factories/HttpCredentialsFactory.kt create mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/exceptions/NotSupportedHelper.kt diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt index a37745c..75bf958 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt @@ -1,19 +1,27 @@ package com.jetpackduba.gitnuro.credentials +import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Config import org.eclipse.jgit.transport.CredentialItem import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.URIish -import javax.inject.Inject +import java.io.* +import java.util.concurrent.TimeUnit -class HttpCredentialsProvider @Inject constructor( - private val credentialsStateManager: CredentialsStateManager +private const val TIMEOUT_MIN = 1L + +class HttpCredentialsProvider @AssistedInject constructor( + private val credentialsStateManager: CredentialsStateManager, + @Assisted val git: Git?, ) : CredentialsProvider() { override fun isInteractive(): Boolean { return true } override fun supports(vararg items: CredentialItem?): Boolean { - println(items) val fields = items.map { credentialItem -> credentialItem?.promptText } return if (fields.isEmpty()) { true @@ -23,30 +31,199 @@ class HttpCredentialsProvider @Inject constructor( fields.contains("Password") } - override fun get(uri: URIish?, vararg items: CredentialItem?): Boolean { - credentialsStateManager.updateState(CredentialsState.HttpCredentialsRequested) + override fun get(uri: URIish, vararg items: CredentialItem): Boolean { + val userItem = items.firstOrNull { it.promptText == "Username" } + val passwordItem = items.firstOrNull { it.promptText == "Password" } - var credentials = credentialsStateManager.currentCredentialsState - while (credentials is CredentialsState.CredentialsRequested) { - credentials = credentialsStateManager.currentCredentialsState + if (userItem !is CredentialItem.Username || passwordItem !is CredentialItem.Password) { + return false } - if (credentials is CredentialsState.HttpCredentialsAccepted) { - val userItem = items.firstOrNull { it?.promptText == "Username" } - val passwordItem = items.firstOrNull { it?.promptText == "Password" } + val externalCredentialsHelper = getExternalCredentialsHelper(uri, git) - if (userItem is CredentialItem.Username && - passwordItem is CredentialItem.Password - ) { + if (externalCredentialsHelper == null) { + val credentials = askForCredentials() + if (credentials is CredentialsState.HttpCredentialsAccepted) { userItem.value = credentials.user passwordItem.value = credentials.password.toCharArray() return true } - } - return false + return false + + } else { + when (handleExternalCredentialHelper(externalCredentialsHelper, uri, items)) { + ExternalCredentialsRequestResult.SUCCESS -> return true + ExternalCredentialsRequestResult.FAIL -> return false + ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED -> { + val credentials = askForCredentials() + if (credentials is CredentialsState.HttpCredentialsAccepted) { + userItem.value = credentials.user + passwordItem.value = credentials.password.toCharArray() + + saveCredentialsInExternalHelper(uri, externalCredentialsHelper, credentials) + + return true + } + + return false + } + } + } } + private fun saveCredentialsInExternalHelper( + uri: URIish, + externalCredentialsHelper: ExternalCredentialsHelper, + credentials: CredentialsState.HttpCredentialsAccepted + ) { + val process = Runtime.getRuntime() + .exec(String.format("${externalCredentialsHelper.path} %s", "store")); + + val output = process.outputStream // write to the input stream of the helper + val bufferedWriter = BufferedWriter(OutputStreamWriter(output)) + + bufferedWriter.use { + bufferedWriter.write("protocol=${uri.scheme}\n") + bufferedWriter.write("host=${uri.host}\n") + + if (externalCredentialsHelper.useHttpPath) { + bufferedWriter.write("path=${uri.path}\n") + } + + bufferedWriter.write("username=${credentials.user}\n") + bufferedWriter.write("password=${credentials.password}\n") + bufferedWriter.write("") + + bufferedWriter.flush() + } + } + + private fun askForCredentials(): CredentialsState { + credentialsStateManager.updateState(CredentialsState.HttpCredentialsRequested) + var credentials = credentialsStateManager.currentCredentialsState + while (credentials is CredentialsState.CredentialsRequested) { + credentials = credentialsStateManager.currentCredentialsState + } + + return credentials + } + + private fun handleExternalCredentialHelper( + externalCredentialsHelper: ExternalCredentialsHelper, + uri: URIish, + items: Array + ): ExternalCredentialsRequestResult { + val process = Runtime.getRuntime() + .exec(String.format("${externalCredentialsHelper.path} %s", "get")) + + val output = process.outputStream // write to the input stream of the helper + val input = process.inputStream // reads from the output stream of the helper + + val bufferedWriter = BufferedWriter(OutputStreamWriter(output)) + val bufferedReader = BufferedReader(InputStreamReader(input)) + + bufferedWriter.use { + bufferedWriter.write("protocol=${uri.scheme}\n") + bufferedWriter.write("host=${uri.host}\n") + + if (externalCredentialsHelper.useHttpPath) { + bufferedWriter.write("path=${uri.path}\n") + } + + bufferedWriter.write("") + + bufferedWriter.flush() + } + + var usernameSet = false + var passwordSet = false + + process.waitFor(TIMEOUT_MIN, TimeUnit.MINUTES) + + // If the process is alive after $TIMEOUT_MIN, it means that it hasn't given an answer and then finished + if(process.isAlive) { + process.destroy() + return ExternalCredentialsRequestResult.FAIL + } + + bufferedReader.use { + var line: String; + while (bufferedReader.readLine().also { line = it } != null && !(usernameSet && passwordSet)) { + if (line.startsWith("username=")) { + val split = line.split("=") + val userName = split.getOrNull(1) ?: return ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED + + val userNameItem = items.firstOrNull { it.promptText == "Username" } + + if (userNameItem is CredentialItem.Username) { + userNameItem.value = userName + usernameSet = true + } + + } else if (line.startsWith("password=")) { + val split = line.split("=") + val password = split.getOrNull(1) ?: return ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED + + val passwordItem = items.firstOrNull { it.promptText == "Password" } + + if (passwordItem is CredentialItem.Password) { + passwordItem.value = password.toCharArray() + passwordSet = true + } + } + } + } + + return if (usernameSet && passwordSet) + ExternalCredentialsRequestResult.SUCCESS + else + ExternalCredentialsRequestResult.CREDENTIALS_NOT_STORED + } + + private fun getExternalCredentialsHelper(uri: URIish, git: Git?): ExternalCredentialsHelper? { + val config = if (git == null) { + val homePath = System.getProperty("user.home") + val configFile = File("$homePath/.gitconfig") + + Config().apply { + if (configFile.exists()) { + fromText(configFile.readText()) + } + } + } else { + git.repository.config + } + + val hostWithProtocol = "${uri.scheme}://${uri.host}" + + val genericCredentialHelper = config.getString("credential", null, "helper") + val uriSpecificCredentialHelper = config.getString("credential", hostWithProtocol, "helper") + val credentialHelperPath = uriSpecificCredentialHelper ?: genericCredentialHelper ?: return null + + if(credentialHelperPath == "cache" || credentialHelperPath == "store") { + throw NotSupportedHelper("Invalid credentials: \"$credentialHelperPath\" is not yet supported") + } + + // Use getString instead of getBoolean as boolean has a default value by we want null if the config field is not set + val uriSpecificUseHttpHelper = config.getString("credential", hostWithProtocol, "useHttpPath") + val genericUseHttpHelper = config.getBoolean("credential", "useHttpPath", false) + + val useHttpPath = uriSpecificUseHttpHelper?.toBoolean() ?: genericUseHttpHelper + + return ExternalCredentialsHelper(credentialHelperPath, useHttpPath) + } +} + +data class ExternalCredentialsHelper( + val path: String, + val useHttpPath: Boolean, +) + +enum class ExternalCredentialsRequestResult { + SUCCESS, + FAIL, + CREDENTIALS_NOT_STORED; } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/di/factories/HttpCredentialsFactory.kt b/src/main/kotlin/com/jetpackduba/gitnuro/di/factories/HttpCredentialsFactory.kt new file mode 100644 index 0000000..d0a7d97 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/di/factories/HttpCredentialsFactory.kt @@ -0,0 +1,10 @@ +package com.jetpackduba.gitnuro.di.factories + +import com.jetpackduba.gitnuro.credentials.HttpCredentialsProvider +import dagger.assisted.AssistedFactory +import org.eclipse.jgit.api.Git + +@AssistedFactory +interface HttpCredentialsFactory { + fun create(git: Git?) : HttpCredentialsProvider +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/exceptions/NotSupportedHelper.kt b/src/main/kotlin/com/jetpackduba/gitnuro/exceptions/NotSupportedHelper.kt new file mode 100644 index 0000000..57a8e7a --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/exceptions/NotSupportedHelper.kt @@ -0,0 +1,3 @@ +package com.jetpackduba.gitnuro.exceptions + +class NotSupportedHelper(message: String) : GitnuroException(message) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/CloneRepositoryUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/CloneRepositoryUseCase.kt index cccc22d..96817a7 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/CloneRepositoryUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/CloneRepositoryUseCase.kt @@ -59,7 +59,7 @@ class CloneRepositoryUseCase @Inject constructor( } } ) - .setTransportConfigCallback { handleTransportUseCase(it) } + .setTransportConfigCallback { handleTransportUseCase(it, null) } .call() ensureActive() diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/DeleteRemoteBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/DeleteRemoteBranchUseCase.kt index b0c9ec6..db556fb 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/DeleteRemoteBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/DeleteRemoteBranchUseCase.kt @@ -27,7 +27,7 @@ class DeleteRemoteBranchUseCase @Inject constructor( val pushResults = git.push() .setTransportConfigCallback { - handleTransportUseCase(it) + handleTransportUseCase(it, git) } .setRefSpecs(refSpec) .setRemote(remoteName) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/FetchAllBranchesUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/FetchAllBranchesUseCase.kt index b20ab6d..ae73d6f 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/FetchAllBranchesUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/FetchAllBranchesUseCase.kt @@ -16,7 +16,7 @@ class FetchAllBranchesUseCase @Inject constructor( git.fetch() .setRemote(remote.name) .setRefSpecs(remote.fetchRefSpecs) - .setTransportConfigCallback { handleTransportUseCase(it) } + .setTransportConfigCallback { handleTransportUseCase(it, git) } .setCredentialsProvider(CredentialsProvider.getDefault()) .call() } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/HandleTransportUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/HandleTransportUseCase.kt index 1e4b0e9..c3c16f0 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/HandleTransportUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/HandleTransportUseCase.kt @@ -2,6 +2,8 @@ package com.jetpackduba.gitnuro.git.remote_operations import com.jetpackduba.gitnuro.credentials.GSessionManager import com.jetpackduba.gitnuro.credentials.HttpCredentialsProvider +import com.jetpackduba.gitnuro.di.factories.HttpCredentialsFactory +import org.eclipse.jgit.api.Git import org.eclipse.jgit.transport.HttpTransport import org.eclipse.jgit.transport.SshTransport import org.eclipse.jgit.transport.Transport @@ -10,13 +12,13 @@ import javax.inject.Provider class HandleTransportUseCase @Inject constructor( private val sessionManager: GSessionManager, - private val httpCredentialsProvider: Provider, + private val httpCredentialsProvider: HttpCredentialsFactory, ) { - operator fun invoke(transport: Transport?) { + operator fun invoke(transport: Transport?, git: Git?) { if (transport is SshTransport) { transport.sshSessionFactory = sessionManager.generateSshSessionFactory() } else if (transport is HttpTransport) { - transport.credentialsProvider = httpCredentialsProvider.get() + transport.credentialsProvider = httpCredentialsProvider.create(git) } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullBranchUseCase.kt index bb1f27c..b5b6d03 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullBranchUseCase.kt @@ -13,7 +13,7 @@ class PullBranchUseCase @Inject constructor( suspend operator fun invoke(git: Git, rebase: Boolean) = withContext(Dispatchers.IO) { val pullResult = git .pull() - .setTransportConfigCallback { handleTransportUseCase(it) } + .setTransportConfigCallback { handleTransportUseCase(it, git) } .setRebase(rebase) .setCredentialsProvider(CredentialsProvider.getDefault()) .call() diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullFromSpecificBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullFromSpecificBranchUseCase.kt index 5187109..db65faf 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullFromSpecificBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PullFromSpecificBranchUseCase.kt @@ -16,7 +16,7 @@ class PullFromSpecificBranchUseCase @Inject constructor( suspend operator fun invoke(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) { val pullResult = git .pull() - .setTransportConfigCallback { handleTransportUseCase(it) } + .setTransportConfigCallback { handleTransportUseCase(it, git) } .setRemote(remoteBranch.remoteName) .setRemoteBranchName(remoteBranch.simpleName) .setRebase(rebase) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt index 5bed992..eb70a4c 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt @@ -22,7 +22,7 @@ class PushBranchUseCase @Inject constructor( if (pushTags) setPushTags() } - .setTransportConfigCallback { handleTransportUseCase(it) } + .setTransportConfigCallback { handleTransportUseCase(it, git) } .call() val results = diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushToSpecificBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushToSpecificBranchUseCase.kt index 0b0e5d0..af8b799 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushToSpecificBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushToSpecificBranchUseCase.kt @@ -27,7 +27,7 @@ class PushToSpecificBranchUseCase @Inject constructor( if (pushTags) setPushTags() } - .setTransportConfigCallback { handleTransportUseCase(it) } + .setTransportConfigCallback { handleTransportUseCase(it, git) } .call() val results =