diff --git a/pom.xml b/pom.xml index 99e80f0..241de91 100644 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,10 @@ org.jenkins-ci.plugins ssh-credentials + + org.jenkins-ci.plugins + git-client + diff --git a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java index 3ec9f44..f25edf4 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java @@ -24,19 +24,26 @@ package com.cloudbees.jenkins.plugins.sshagent.exec; -import hudson.AbortException; -import hudson.FilePath; -import hudson.Launcher; -import hudson.model.TaskListener; -import hudson.slaves.WorkspaceList; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; +import hudson.AbortException; +import hudson.FilePath; +import hudson.Functions; +import hudson.Launcher; +import hudson.model.TaskListener; +import hudson.plugins.git.GitTool; +import hudson.slaves.WorkspaceList; + /** * Runs a native SSH agent installed on a system. */ @@ -46,15 +53,46 @@ public final class ExecRemoteAgent implements Serializable { /** Agent environment used for {@code ssh-add} and {@code ssh-agent -k}. */ private final Map agentEnv; + private final String sshAgentExe; public ExecRemoteAgent(Launcher launcher, TaskListener listener) throws IOException, InterruptedException { + sshAgentExe = !Functions.isWindows() ? "ssh-agent" : searchSSHAgentExeForWindows(launcher, listener); + String agentOutput = executeCommand(launcher, listener, sshAgentExe); + agentEnv = parseAgentEnv(agentOutput, listener); // TODO could include local filenames, better to look up remote charset + } + + private static String executeCommand(Launcher launcher, TaskListener listener, String... cmd) + throws IOException, InterruptedException, AbortException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - if (launcher.launch().cmds("ssh-agent").stdout(baos).start() - .joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) { + if (launcher.launch().cmds(cmd).stdout(baos).start().joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) { String reason = new String(baos.toByteArray(), StandardCharsets.US_ASCII); throw new AbortException("Failed to run ssh-agent: " + reason); } - agentEnv = parseAgentEnv(new String(baos.toByteArray(), StandardCharsets.US_ASCII), listener); // TODO could include local filenames, better to look up remote charset + return new String(baos.toByteArray(), StandardCharsets.US_ASCII); + } + + private static final Path GIT_SSH_AGENT_PATH_WINDOWS = Path.of("usr", "bin", "ssh-agent.exe"); + + private static String searchSSHAgentExeForWindows(Launcher launcher, TaskListener listener) + throws IOException, InterruptedException { + Optional defaultGitHome = Optional.ofNullable(GitTool.getDefaultInstallation()) // + .map(GitTool::getHome).map(Path::of); + if (defaultGitHome.isPresent()) { + Path sshAgentExe = defaultGitHome.get().resolve(GIT_SSH_AGENT_PATH_WINDOWS); + if (Files.isRegularFile(sshAgentExe)) { + return sshAgentExe.toString(); + } + } + String gitPaths = executeCommand(launcher, listener, "where", "git"); + List paths = gitPaths.lines().map(Path::of).toList(); + for (Path gitExe : paths) { + Path sshAgentExe = gitExe.getParent().getParent().resolve(GIT_SSH_AGENT_PATH_WINDOWS); + if (Files.exists(sshAgentExe)) { + return sshAgentExe.toString(); + } + } + throw new IllegalStateException( + "Executing with default ssh-agent on Windows is not supported and an alternative implementation from a git installation is not available."); } /** @@ -84,10 +122,10 @@ public void addIdentity(String privateKey, final String passphrase, String comme env.put("DISPLAY", "bogus"); // just to force using SSH_ASKPASS env.put("SSH_ASKPASS", askpass.getRemote()); } - + // as the next command is in quiet mode, we just add a message to the log listener.getLogger().println("Running ssh-add (command line suppressed)"); - + if (launcher.launch().quiet(true).cmds("ssh-add", keyFile.getRemote()).envs(env) .stdout(listener).start().joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) { throw new AbortException("Failed to run ssh-add"); @@ -117,7 +155,7 @@ public void stop(Launcher launcher, TaskListener listener) throws IOException, I throw new AbortException("Failed to run ssh-agent -k"); } } - + /** * Parses ssh-agent output. */ @@ -133,10 +171,10 @@ private Map parseAgentEnv(String agentOutput, TaskListener listen // get SSH_AGENT_PID env.put(AgentPidVar, getAgentValue(agentOutput, AgentPidVar)); listener.getLogger().println(AgentPidVar + "=" + env.get(AgentPidVar)); - + return env; } - + /** * Parses a value from ssh-agent output. */ @@ -145,16 +183,16 @@ private String getAgentValue(String agentOutput, String envVar) { int end = agentOutput.indexOf(';', pos); return agentOutput.substring(pos, end); } - + /** - * Creates a self-deleting script for SSH_ASKPASS. Self-deleting to be able to detect a wrong passphrase. + * Creates a self-deleting script for SSH_ASKPASS. Self-deleting to be able to detect a wrong passphrase. */ private FilePath createAskpassScript(FilePath temp) throws IOException, InterruptedException { // TODO: assuming that ssh-add runs the script in shell even on Windows, not cmd // for cmd following could work // suffix = ".bat"; // script = "@ECHO %SSH_PASSPHRASE%\nDEL \"" + askpass.getAbsolutePath() + "\"\n"; - + FilePath askpass = temp.createTextTempFile("askpass_", ".sh", "#!/bin/sh\necho \"$SSH_PASSPHRASE\"\nrm \"$0\"\n"); // executable only for a current user