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"]);