diff --git a/cli/pom.xml b/cli/pom.xml index d5e4528b1efe2ca28ba379e1f243bd57c8a387fe..8abde68d975b29eb61c03ce74141d0f074033f66 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -50,6 +50,17 @@ 1.24 + org.apache.sshd + sshd-core + 1.2.0 + true + + + org.slf4j + slf4j-nop + true + + org.jenkins-ci trilead-ssh2 build214-jenkins-1 diff --git a/cli/src/main/java/hudson/cli/CLI.java b/cli/src/main/java/hudson/cli/CLI.java index edc1ba35b62c53635ec10c6a5d1e037acc29c305..3d16055904c85851a1f5dea7d1ff8200537730c2 100644 --- a/cli/src/main/java/hudson/cli/CLI.java +++ b/cli/src/main/java/hudson/cli/CLI.java @@ -32,6 +32,7 @@ import hudson.remoting.RemoteInputStream; import hudson.remoting.RemoteOutputStream; import hudson.remoting.SocketChannelStream; import hudson.remoting.SocketOutputStream; +import hudson.util.QuotedStringTokenizer; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -57,6 +58,8 @@ import java.io.StringReader; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLConnection; import java.security.GeneralSecurityException; @@ -70,11 +73,23 @@ import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Properties; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; import static java.util.logging.Level.*; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier; +import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.future.WaitableFuture; +import org.apache.sshd.common.util.io.NoCloseInputStream; +import org.apache.sshd.common.util.io.NoCloseOutputStream; /** * CLI entry point to Jenkins. @@ -403,12 +418,21 @@ public class CLI implements AutoCloseable { boolean tryLoadPKey = true; + boolean useRemoting = false; + + String user = null; + while(!args.isEmpty()) { String head = args.get(0); if (head.equals("-version")) { System.out.println("Version: "+computeVersion()); return 0; } + if (head.equals("-remoting")) { + useRemoting = true; + args = args.subList(1,args.size()); + continue; + } if(head.equals("-s") && args.size()>=2) { url = args.get(1); args = args.subList(2,args.size()); @@ -446,6 +470,11 @@ public class CLI implements AutoCloseable { sshAuthRequestedExplicitly = true; continue; } + if (head.equals("-user") && args.size() >= 2) { + user = args.get(1); + args = args.subList(2, args.size()); + continue; + } if(head.equals("-p") && args.size()>=2) { httpProxy = args.get(1); args = args.subList(2,args.size()); @@ -465,6 +494,19 @@ public class CLI implements AutoCloseable { if (tryLoadPKey && !provider.hasKeys()) provider.readFromDefaultLocations(); + if (!useRemoting) { + if (user == null) { + // TODO SshCliAuthenticator already autodetects the user based on public key; why cannot AsynchronousCommand.getCurrentUser do the same? + System.err.println("-user required when not using -remoting"); + return -1; + } + return sshConnection(url, user, args, provider); + } + + if (user != null) { + System.err.println("Warning: -user ignored when using -remoting"); + } + CLIConnectionFactory factory = new CLIConnectionFactory().url(url).httpsProxyTunnel(httpProxy); String userInfo = new URL(url).getUserInfo(); if (userInfo != null) { @@ -507,6 +549,75 @@ public class CLI implements AutoCloseable { } } + private static int sshConnection(String jenkinsUrl, String user, List args, PrivateKeyProvider provider) throws IOException { + URL url = new URL(jenkinsUrl + "/login"); + URLConnection conn = url.openConnection(); + String endpointDescription = conn.getHeaderField("X-SSH-Endpoint"); + + if (endpointDescription == null) { + System.err.println("No header 'X-SSH-Endpoint' returned by Jenkins"); + return -1; + } + + System.err.println("Connecting to: " + endpointDescription); + + int sshPort = Integer.valueOf(endpointDescription.split(":")[1]); + String sshHost = endpointDescription.split(":")[0]; + + StringBuilder command = new StringBuilder(); + + for (String arg : args) { + command.append(QuotedStringTokenizer.quote(arg)); + command.append(' '); + } + + try(SshClient client = SshClient.setUpDefaultClient()) { + + KnownHostsServerKeyVerifier verifier = new DefaultKnownHostsServerKeyVerifier(new ServerKeyVerifier() { + @Override + public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) { + /** unknown key is okay, but log */ + LOGGER.log(Level.WARNING, "Unknown host key for {0}", remoteAddress.toString()); + // TODO should not trust unknown hosts by default; this should be opt-in + return true; + } + }, true); + + client.setServerKeyVerifier(verifier); + client.start(); + + ConnectFuture cf = client.connect(user, sshHost, sshPort); + cf.await(); + try (ClientSession session = cf.getSession()) { + for (KeyPair pair : provider.getKeys()) { + System.err.println("Offering " + pair.getPrivate().getAlgorithm() + " private key"); + session.addPublicKeyIdentity(pair); + } + session.auth().verify(10000L); + + try (ClientChannel channel = session.createExecChannel(command.toString())) { + channel.setIn(new NoCloseInputStream(System.in)); + channel.setOut(new NoCloseOutputStream(System.out)); + channel.setErr(new NoCloseOutputStream(System.err)); + WaitableFuture wf = channel.open(); + wf.await(); + + Set waitMask = channel.waitFor(Collections.singletonList(ClientChannelEvent.CLOSED), 0L); + + if(waitMask.contains(ClientChannelEvent.TIMEOUT)) { + throw new SocketTimeoutException("Failed to retrieve command result in time: " + command); + } + + Integer exitStatus = channel.getExitStatus(); + return exitStatus; + + } + } finally { + client.stop(); + } + } + } + private static String computeVersion() { Properties props = new Properties(); try { diff --git a/core/src/main/java/hudson/util/QuotedStringTokenizer.java b/cli/src/main/java/hudson/util/QuotedStringTokenizer.java similarity index 100% rename from core/src/main/java/hudson/util/QuotedStringTokenizer.java rename to cli/src/main/java/hudson/util/QuotedStringTokenizer.java diff --git a/cli/src/main/resources/hudson/cli/client/Messages.properties b/cli/src/main/resources/hudson/cli/client/Messages.properties index 98dee46cdb74f879a42521639b214ff65c537a5e..d84fec726939bf248586737201f7492f1b587b6f 100644 --- a/cli/src/main/resources/hudson/cli/client/Messages.properties +++ b/cli/src/main/resources/hudson/cli/client/Messages.properties @@ -6,6 +6,8 @@ CLI.Usage=Jenkins CLI\n\ -p HOST:PORT : HTTP proxy host and port for HTTPS proxy tunneling. See https://jenkins.io/redirect/cli-https-proxy-tunnel\n\ -noCertificateCheck : bypass HTTPS certificate check entirely. Use with caution\n\ -noKeyAuth : don't try to load the SSH authentication private key. Conflicts with -i\n\ + -remoting : use deprecated Remoting channel protocol (if enabled on server; for compatibility with legacy commands or command modes only)\n\ + -user : specify user (for SSH mode, not -remoting)\n\ \n\ The available commands depend on the server. Run the 'help' command to\n\ see the list. diff --git a/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy b/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy index b082cd03e5388a55a0335d0b7ab61063f2eb0412..dcfba02ed84075531e8d038ce4faec03fa7a28e0 100644 --- a/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy +++ b/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy @@ -14,8 +14,10 @@ import hudson.tasks.Builder import hudson.tasks.Shell import jenkins.model.JenkinsLocationConfiguration import org.junit.Assert +import org.junit.ClassRule import org.junit.Rule import org.junit.Test +import org.jvnet.hudson.test.BuildWatcher import org.jvnet.hudson.test.JenkinsRule import org.jvnet.hudson.test.TestBuilder @@ -26,6 +28,9 @@ public class SetBuildParameterCommandTest { @Rule public JenkinsRule j = new JenkinsRule(); + @ClassRule + public static BuildWatcher buildWatcher = new BuildWatcher(); + @Test public void cli() { JenkinsLocationConfiguration.get().url = j.URL; @@ -42,9 +47,9 @@ public class SetBuildParameterCommandTest { }); List pd = [new StringParameterDefinition("a", ""), new StringParameterDefinition("b", "")]; p.addProperty(new ParametersDefinitionProperty(pd)) - p.buildersList.add(createScriptBuilder("java -jar cli.jar set-build-parameter a b")) - p.buildersList.add(createScriptBuilder("java -jar cli.jar set-build-parameter a x")) - p.buildersList.add(createScriptBuilder("java -jar cli.jar set-build-parameter b y")) + p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")) + p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a x")) + p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter b y")) def r = [:]; @@ -54,11 +59,12 @@ public class SetBuildParameterCommandTest { assert r.equals(["a":"x", "b":"y"]); if (Functions.isWindows()) { - p.buildersList.add(new BatchFile("set BUILD_NUMBER=1\r\njava -jar cli.jar set-build-parameter a b")) + p.buildersList.add(new BatchFile("set BUILD_NUMBER=1\r\njava -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")) } else { - p.buildersList.add(new Shell("BUILD_NUMBER=1 java -jar cli.jar set-build-parameter a b")) + p.buildersList.add(new Shell("BUILD_NUMBER=1 java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")) } def b2 = j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); + j.assertLogContains("#1 is not currently being built", b2) r = [:]; b.getAction(ParametersAction.class).parameters.each { v -> r[v.name]=v.value } assert r.equals(["a":"x", "b":"y"]);