Skip to content

Commit

Permalink
[JENKINS-41745] Make jenkins-cli.jar connect to the SSH port by default.
Browse files Browse the repository at this point in the history
  • Loading branch information
jglick committed Mar 10, 2017
1 parent 6e39dd3 commit 492dbbe
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 5 deletions.
11 changes: 11 additions & 0 deletions cli/pom.xml
Expand Up @@ -50,6 +50,17 @@
<version>1.24</version>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId>
<version>1.2.0</version> <!-- TODO 1.3.0 requires Java 8 -->
<optional>true</optional> <!-- do not expose to core -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<optional>true</optional> <!-- ditto -->
</dependency>
<dependency> <!-- TODO remove and replace PrivateKeyProvider with SecurityUtils.createFileKeyPairProvider() as in SshClient -->
<groupId>org.jenkins-ci</groupId>
<artifactId>trilead-ssh2</artifactId>
<version>build214-jenkins-1</version>
Expand Down
111 changes: 111 additions & 0 deletions cli/src/main/java/hudson/cli/CLI.java
Expand Up @@ -32,6 +32,7 @@
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;
Expand All @@ -57,6 +58,8 @@
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;
Expand All @@ -70,11 +73,23 @@
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.
Expand Down Expand Up @@ -403,12 +418,21 @@ public static int _main(String[] _args) throws Exception {

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());
Expand Down Expand Up @@ -446,6 +470,11 @@ public boolean verify(String s, SSLSession sslSession) {
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());
Expand All @@ -465,6 +494,19 @@ public boolean verify(String s, SSLSession sslSession) {
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) {
Expand Down Expand Up @@ -507,6 +549,75 @@ public boolean verify(String s, SSLSession sslSession) {
}
}

private static int sshConnection(String jenkinsUrl, String user, List<String> 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 {
Expand Down
2 changes: 2 additions & 0 deletions cli/src/main/resources/hudson/cli/client/Messages.properties
Expand Up @@ -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.
Expand Down
Expand Up @@ -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

Expand All @@ -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;
Expand All @@ -42,9 +47,9 @@ public class SetBuildParameterCommandTest {
});
List<ParameterDefinition> 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 = [:];

Expand All @@ -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"]);
Expand Down

0 comments on commit 492dbbe

Please sign in to comment.