Gitnuro/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt
2023-04-17 17:03:06 +02:00

239 lines
9.1 KiB
Kotlin

package com.jetpackduba.gitnuro.credentials
import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper
import com.jetpackduba.gitnuro.managers.IShellManager
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 java.io.*
import java.util.concurrent.TimeUnit
import kotlin.coroutines.cancellation.CancellationException
private const val TIMEOUT_MIN = 1L
class HttpCredentialsProvider @AssistedInject constructor(
private val credentialsStateManager: CredentialsStateManager,
private val shellManager: IShellManager,
@Assisted val git: Git?,
) : CredentialsProvider() {
override fun isInteractive(): Boolean {
return true
}
override fun supports(vararg items: CredentialItem?): Boolean {
val fields = items.map { credentialItem -> credentialItem?.promptText }
return if (fields.isEmpty()) {
true
} else
fields.size == 2 &&
fields.contains("Username") &&
fields.contains("Password")
}
override fun get(uri: URIish, vararg items: CredentialItem): Boolean {
val userItem = items.firstOrNull { it.promptText == "Username" }
val passwordItem = items.firstOrNull { it.promptText == "Password" }
if (userItem !is CredentialItem.Username || passwordItem !is CredentialItem.Password) {
return false
}
val externalCredentialsHelper = getExternalCredentialsHelper(uri, git)
if (externalCredentialsHelper == null) {
val credentials = askForCredentials()
if (credentials is CredentialsAccepted.HttpCredentialsAccepted) {
userItem.value = credentials.user
passwordItem.value = credentials.password.toCharArray()
return true
} else if (credentials is CredentialsState.CredentialsDenied) {
throw CancellationException("Credentials denied")
}
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 CredentialsAccepted.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: CredentialsAccepted.HttpCredentialsAccepted
) {
val process = shellManager.runCommandProcess(listOf(externalCredentialsHelper.path, "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(CredentialsRequested.HttpCredentialsRequested)
var credentials = credentialsStateManager.currentCredentialsState
while (credentials is CredentialsRequested) {
credentials = credentialsStateManager.currentCredentialsState
}
return credentials
}
private fun handleExternalCredentialHelper(
externalCredentialsHelper: ExternalCredentialsHelper,
uri: URIish,
items: Array<out CredentialItem>
): ExternalCredentialsRequestResult {
val process = shellManager.runCommandProcess(listOf(externalCredentialsHelper.path, "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 = checkNotNull(it) { "Cancelled authentication" }
} != 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")
var credentialHelperPath = uriSpecificCredentialHelper ?: genericCredentialHelper ?: return null
if (credentialHelperPath == "cache" || credentialHelperPath == "store") {
throw NotSupportedHelper("Invalid credentials helper: \"$credentialHelperPath\" is not yet supported")
}
// TODO Try to use "git-credential-manager-core" when "manager-core" is detected. Works for linux but requires testing for mac/windows
if (credentialHelperPath == "manager-core") {
throw NotSupportedHelper("Invalid credentials helper \"$credentialHelperPath\". Please specify the full path of Git Credential Manager in your .gitconfig")
}
// 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;
}