diff --git a/cli/pom.xml b/cli/pom.xml
index 42955c8b351879181e91b949d83592e608c7eacc..9d8fe693390cbe5443c27ea1a2385361784ba345 100644
--- a/cli/pom.xml
+++ b/cli/pom.xml
@@ -34,6 +34,10 @@
commons-codec
1.4
+
+ commons-io
+ commons-io
+
${project.groupId}
remoting
@@ -50,6 +54,17 @@
1.24
+ org.apache.sshd
+ sshd-core
+ 1.2.0
+ true
+
+
+ org.slf4j
+ slf4j-jdk14
+ 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 4df7c421b6c9d81f8751a3cb716f09ed76d01bbe..c9d7149ca3d4cb9cb4bcf5b4520254080949d661 100644
--- a/cli/src/main/java/hudson/cli/CLI.java
+++ b/cli/src/main/java/hudson/cli/CLI.java
@@ -32,7 +32,6 @@ import hudson.remoting.RemoteInputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.SocketChannelStream;
import hudson.remoting.SocketOutputStream;
-
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HostnameVerifier;
@@ -59,6 +58,7 @@ import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
+import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PublicKey;
@@ -72,11 +72,11 @@ import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import static java.util.logging.Level.*;
+import org.apache.commons.io.FileUtils;
/**
* CLI entry point to Jenkins.
@@ -92,6 +92,11 @@ public class CLI implements AutoCloseable {
private final String httpsProxyTunnel;
private final String authorization;
+ /**
+ * For tests only.
+ * @deprecated Specific to {@link Mode#REMOTING}.
+ */
+ @Deprecated
public CLI(URL jenkins) throws IOException, InterruptedException {
this(jenkins,null);
}
@@ -113,26 +118,27 @@ public class CLI implements AutoCloseable {
public CLI(URL jenkins, ExecutorService exec, String httpsProxyTunnel) throws IOException, InterruptedException {
this(new CLIConnectionFactory().url(jenkins).executorService(exec).httpsProxyTunnel(httpsProxyTunnel));
}
-
+
+ /**
+ * @deprecated Specific to {@link Mode#REMOTING}.
+ */
+ @Deprecated
/*package*/ CLI(CLIConnectionFactory factory) throws IOException, InterruptedException {
URL jenkins = factory.jenkins;
this.httpsProxyTunnel = factory.httpsProxyTunnel;
this.authorization = factory.authorization;
ExecutorService exec = factory.exec;
- String url = jenkins.toExternalForm();
- if(!url.endsWith("/")) url+='/';
-
ownsPool = exec==null;
pool = exec!=null ? exec : Executors.newCachedThreadPool(new NamingThreadFactory(Executors.defaultThreadFactory(), "CLI.pool"));
Channel _channel;
try {
- _channel = connectViaCliPort(jenkins, getCliTcpPort(url));
+ _channel = connectViaCliPort(jenkins, getCliTcpPort(jenkins));
} catch (IOException e) {
- LOGGER.log(Level.FINE,"Failed to connect via CLI port. Falling back to HTTP",e);
+ LOGGER.log(Level.FINE, "Failed to connect via CLI port. Falling back to HTTP", e);
try {
- _channel = connectViaHttp(url);
+ _channel = connectViaHttp(jenkins);
} catch (IOException e2) {
e.addSuppressed(e2);
throw e;
@@ -147,13 +153,15 @@ public class CLI implements AutoCloseable {
throw new IOException(Messages.CLI_VersionMismatch());
}
- private Channel connectViaHttp(String url) throws IOException {
- LOGGER.log(FINE, "Trying to connect to {0} via HTTP", url);
- url+="cli";
- URL jenkins = new URL(url);
+ /**
+ * @deprecated Specific to {@link Mode#REMOTING}.
+ */
+ @Deprecated
+ private Channel connectViaHttp(URL url) throws IOException {
+ LOGGER.log(FINE, "Trying to connect to {0} via Remoting over HTTP", url);
- FullDuplexHttpStream con = new FullDuplexHttpStream(jenkins,authorization);
- Channel ch = new Channel("Chunked connection to "+jenkins,
+ FullDuplexHttpStream con = new FullDuplexHttpStream(url, "cli?remoting=true", authorization);
+ Channel ch = new Channel("Chunked connection to " + url,
pool,con.getInputStream(),con.getOutputStream());
final long interval = 15*1000;
final long timeout = (interval * 3) / 4;
@@ -166,8 +174,17 @@ public class CLI implements AutoCloseable {
return ch;
}
+ /**
+ * @deprecated Specific to {@link Mode#REMOTING}.
+ */
+ @Deprecated
private Channel connectViaCliPort(URL jenkins, CliPort clip) throws IOException {
- LOGGER.log(FINE, "Trying to connect directly via TCP/IP to {0}", clip.endpoint);
+ LOGGER.log(FINE, "Trying to connect directly via Remoting over TCP/IP to {0}", clip.endpoint);
+
+ if (authorization != null) {
+ LOGGER.warning("-auth ignored when using JNLP agent port");
+ }
+
final Socket s = new Socket();
// this prevents a connection from silently terminated by the router in between or the other peer
// and that goes without unnoticed. However, the time out is often very long (for example 2 hours
@@ -265,13 +282,14 @@ public class CLI implements AutoCloseable {
/**
* If the server advertises CLI endpoint, returns its location.
+ * @deprecated Specific to {@link Mode#REMOTING}.
*/
- protected CliPort getCliTcpPort(String url) throws IOException {
- URL _url = new URL(url);
- if (_url.getHost()==null || _url.getHost().length()==0) {
+ @Deprecated
+ protected CliPort getCliTcpPort(URL url) throws IOException {
+ if (url.getHost()==null || url.getHost().length()==0) {
throw new IOException("Invalid URL: "+url);
}
- URLConnection head = _url.openConnection();
+ URLConnection head = url.openConnection();
try {
head.connect();
} catch (IOException e) {
@@ -385,12 +403,6 @@ public class CLI implements AutoCloseable {
}
public static void main(final String[] _args) throws Exception {
- Logger l = Logger.getLogger(ROOT_LOGGER_NAME);
- l.setLevel(SEVERE);
- ConsoleHandler h = new ConsoleHandler();
- h.setLevel(SEVERE);
- l.addHandler(h);
-
try {
System.exit(_main(_args));
} catch (Throwable t) {
@@ -400,6 +412,7 @@ public class CLI implements AutoCloseable {
}
}
+ private enum Mode {HTTP, SSH, REMOTING}
public static int _main(String[] _args) throws Exception {
List args = Arrays.asList(_args);
PrivateKeyProvider provider = new PrivateKeyProvider();
@@ -413,19 +426,52 @@ public class CLI implements AutoCloseable {
boolean tryLoadPKey = true;
+ Mode mode = null;
+
+ String user = null;
+ String auth = null;
+ boolean strictHostKey = false;
+
while(!args.isEmpty()) {
String head = args.get(0);
if (head.equals("-version")) {
System.out.println("Version: "+computeVersion());
return 0;
}
+ if (head.equals("-http")) {
+ if (mode != null) {
+ printUsage("-http clashes with previously defined mode " + mode);
+ return -1;
+ }
+ mode = Mode.HTTP;
+ args = args.subList(1, args.size());
+ continue;
+ }
+ if (head.equals("-ssh")) {
+ if (mode != null) {
+ printUsage("-ssh clashes with previously defined mode " + mode);
+ return -1;
+ }
+ mode = Mode.SSH;
+ args = args.subList(1, args.size());
+ continue;
+ }
+ if (head.equals("-remoting")) {
+ if (mode != null) {
+ printUsage("-remoting clashes with previously defined mode " + mode);
+ return -1;
+ }
+ mode = Mode.REMOTING;
+ args = args.subList(1, args.size());
+ continue;
+ }
if(head.equals("-s") && args.size()>=2) {
url = args.get(1);
args = args.subList(2,args.size());
continue;
}
if (head.equals("-noCertificateCheck")) {
- System.err.println("Skipping HTTPS certificate checks altogether. Note that this is not secure at all.");
+ LOGGER.info("Skipping HTTPS certificate checks altogether. Note that this is not secure at all.");
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[]{new NoCheckTrustManager()}, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
@@ -456,18 +502,35 @@ public class CLI implements AutoCloseable {
sshAuthRequestedExplicitly = true;
continue;
}
+ if (head.equals("-strictHostKey")) {
+ strictHostKey = true;
+ args = args.subList(1, args.size());
+ continue;
+ }
+ if (head.equals("-user") && args.size() >= 2) {
+ user = args.get(1);
+ args = args.subList(2, args.size());
+ continue;
+ }
+ if (head.equals("-auth") && args.size() >= 2) {
+ auth = 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());
continue;
}
- if(head.equals("-v")) {
- args = args.subList(1,args.size());
- Logger l = Logger.getLogger(ROOT_LOGGER_NAME);
- l.setLevel(FINEST);
- for (Handler h : l.getHandlers()) {
- h.setLevel(FINEST);
+ if (head.equals("-logger") && args.size() >= 2) {
+ Level level = parse(args.get(1));
+ for (Handler h : Logger.getLogger("").getHandlers()) {
+ h.setLevel(level);
+ }
+ for (Logger logger : new Logger[] {LOGGER, PlainCLIProtocol.LOGGER, Logger.getLogger("org.apache.sshd")}) { // perhaps also Channel
+ logger.setLevel(level);
}
+ args = args.subList(2, args.size());
continue;
}
break;
@@ -478,16 +541,53 @@ public class CLI implements AutoCloseable {
return -1;
}
+ if (!url.endsWith("/")) {
+ url += '/';
+ }
+
if(args.isEmpty())
args = Arrays.asList("help"); // default to help
if (tryLoadPKey && !provider.hasKeys())
provider.readFromDefaultLocations();
+ if (mode == null) {
+ mode = Mode.HTTP;
+ }
+
+ LOGGER.log(FINE, "using connection mode {0}", mode);
+
+ if (user != null && auth != null) {
+ LOGGER.warning("-user and -auth are mutually exclusive");
+ }
+
+ if (mode == Mode.SSH) {
+ if (user == null) {
+ // TODO SshCliAuthenticator already autodetects the user based on public key; why cannot AsynchronousCommand.getCurrentUser do the same?
+ LOGGER.warning("-user required when using -ssh");
+ return -1;
+ }
+ return SSHCLI.sshConnection(url, user, args, provider, strictHostKey);
+ }
+
+ if (strictHostKey) {
+ LOGGER.warning("-strictHostKey meaningful only with -ssh");
+ }
+
+ if (user != null) {
+ LOGGER.warning("Warning: -user ignored unless using -ssh");
+ }
+
CLIConnectionFactory factory = new CLIConnectionFactory().url(url).httpsProxyTunnel(httpProxy);
String userInfo = new URL(url).getUserInfo();
if (userInfo != null) {
factory = factory.basicAuth(userInfo);
+ } else if (auth != null) {
+ factory = factory.basicAuth(auth.startsWith("@") ? FileUtils.readFileToString(new File(auth.substring(1))).trim() : auth);
+ }
+
+ if (mode == Mode.HTTP) {
+ return plainHttpConnection(url, args, factory);
}
CLI cli = factory.connect();
@@ -498,22 +598,21 @@ public class CLI implements AutoCloseable {
cli.authenticate(provider.getKeys());
} catch (IllegalStateException e) {
if (sshAuthRequestedExplicitly) {
- System.err.println("The server doesn't support public key authentication");
+ LOGGER.warning("The server doesn't support public key authentication");
return -1;
}
} catch (UnsupportedOperationException e) {
if (sshAuthRequestedExplicitly) {
- System.err.println("The server doesn't support public key authentication");
+ LOGGER.warning("The server doesn't support public key authentication");
return -1;
}
} catch (GeneralSecurityException e) {
if (sshAuthRequestedExplicitly) {
- System.err.println(e.getMessage());
- LOGGER.log(FINE,e.getMessage(),e);
+ LOGGER.log(WARNING, null, e);
return -1;
}
- System.err.println("[WARN] Failed to authenticate with your SSH keys. Proceeding as anonymous");
- LOGGER.log(FINE,"Failed to authenticate with your SSH keys.",e);
+ LOGGER.warning("Failed to authenticate with your SSH keys. Proceeding as anonymous");
+ LOGGER.log(FINE, null, e);
}
}
@@ -526,6 +625,72 @@ public class CLI implements AutoCloseable {
}
}
+ private static int plainHttpConnection(String url, List args, CLIConnectionFactory factory) throws IOException, InterruptedException {
+ LOGGER.log(FINE, "Trying to connect to {0} via plain protocol over HTTP", url);
+ FullDuplexHttpStream streams = new FullDuplexHttpStream(new URL(url), "cli?remoting=false", factory.authorization);
+ class ClientSideImpl extends PlainCLIProtocol.ClientSide {
+ boolean complete;
+ int exit = -1;
+ ClientSideImpl(InputStream is, OutputStream os) throws IOException {
+ super(is, os);
+ if (is.read() != 0) { // cf. FullDuplexHttpService
+ throw new IOException("expected to see initial zero byte; perhaps you are connecting to an old server which does not support -http?");
+ }
+ }
+ @Override
+ protected void onExit(int code) {
+ this.exit = code;
+ finished();
+ }
+ @Override
+ protected void onStdout(byte[] chunk) throws IOException {
+ System.out.write(chunk);
+ }
+ @Override
+ protected void onStderr(byte[] chunk) throws IOException {
+ System.err.write(chunk);
+ }
+ @Override
+ protected void handleClose() {
+ finished();
+ }
+ private synchronized void finished() {
+ complete = true;
+ notifyAll();
+ }
+ }
+ try (final ClientSideImpl connection = new ClientSideImpl(streams.getInputStream(), streams.getOutputStream())) {
+ for (String arg : args) {
+ connection.sendArg(arg);
+ }
+ connection.sendEncoding(Charset.defaultCharset().name());
+ connection.sendLocale(Locale.getDefault().toString());
+ connection.sendStart();
+ connection.begin();
+ final OutputStream stdin = connection.streamStdin();
+ new Thread("input reader") {
+ @Override
+ public void run() {
+ try {
+ int c;
+ while ((c = System.in.read()) != -1) { // TODO use InputStream.available
+ stdin.write(c);
+ }
+ connection.sendEndStdin();
+ } catch (IOException x) {
+ LOGGER.log(Level.WARNING, null, x);
+ }
+ }
+ }.start();
+ synchronized (connection) {
+ while (!connection.complete) {
+ connection.wait();
+ }
+ }
+ return connection.exit;
+ }
+ }
+
private static String computeVersion() {
Properties props = new Properties();
try {
@@ -570,7 +735,9 @@ public class CLI implements AutoCloseable {
*
* @return
* identity of the server represented as a public key.
+ * @deprecated Specific to {@link Mode#REMOTING}.
*/
+ @Deprecated
public PublicKey authenticate(Iterable privateKeys) throws IOException, GeneralSecurityException {
Pipe c2s = Pipe.createLocalToRemote();
Pipe s2c = Pipe.createRemoteToLocal();
@@ -596,6 +763,10 @@ public class CLI implements AutoCloseable {
}
}
+ /**
+ * @deprecated Specific to {@link Mode#REMOTING}.
+ */
+ @Deprecated
public PublicKey authenticate(KeyPair key) throws IOException, GeneralSecurityException {
return authenticate(Collections.singleton(key));
}
@@ -605,6 +776,5 @@ public class CLI implements AutoCloseable {
System.err.println(Messages.CLI_Usage());
}
- private static final Logger LOGGER = Logger.getLogger(CLI.class.getName());
- private static final String ROOT_LOGGER_NAME = CLI.class.getPackage().getName();
+ static final Logger LOGGER = Logger.getLogger(CLI.class.getName());
}
diff --git a/cli/src/main/java/hudson/cli/CLIConnectionFactory.java b/cli/src/main/java/hudson/cli/CLIConnectionFactory.java
index a2e5681039effafbb22765a249a508fe8fdb22b2..894e4c0fdac60ff446e3a67ecf68e44192d93f97 100644
--- a/cli/src/main/java/hudson/cli/CLIConnectionFactory.java
+++ b/cli/src/main/java/hudson/cli/CLIConnectionFactory.java
@@ -32,6 +32,7 @@ public class CLIConnectionFactory {
/**
* This {@link ExecutorService} is used to execute closures received from the server.
+ * Used only in Remoting mode.
*/
public CLIConnectionFactory executorService(ExecutorService es) {
this.exec = es;
@@ -59,15 +60,24 @@ public class CLIConnectionFactory {
/**
* Convenience method to call {@link #authorization} with the HTTP basic authentication.
+ * Currently unused.
*/
public CLIConnectionFactory basicAuth(String username, String password) {
return basicAuth(username+':'+password);
}
+ /**
+ * Convenience method to call {@link #authorization} with the HTTP basic authentication.
+ * Cf. {@code BasicHeaderApiTokenAuthenticator}.
+ */
public CLIConnectionFactory basicAuth(String userInfo) {
return authorization("Basic " + new String(Base64.encodeBase64((userInfo).getBytes())));
}
-
+
+ /**
+ * @deprecated Specific to Remoting-based protocol.
+ */
+ @Deprecated
public CLI connect() throws IOException, InterruptedException {
return new CLI(this);
}
diff --git a/cli/src/main/java/hudson/cli/CliEntryPoint.java b/cli/src/main/java/hudson/cli/CliEntryPoint.java
index 57bd42e553b77336c2a0db0085ae4ce0474c76b2..fe1f6c835097888be71632babbe31f1b6530fab5 100644
--- a/cli/src/main/java/hudson/cli/CliEntryPoint.java
+++ b/cli/src/main/java/hudson/cli/CliEntryPoint.java
@@ -34,7 +34,9 @@ import java.util.Locale;
* Remotable interface for CLI entry point on the server side.
*
* @author Kohsuke Kawaguchi
+ * @deprecated Specific to Remoting-based protocol.
*/
+@Deprecated
public interface CliEntryPoint {
/**
* Just like the static main method.
diff --git a/cli/src/main/java/hudson/cli/CliPort.java b/cli/src/main/java/hudson/cli/CliPort.java
index 8faab7de41bee0c88fa90cdaa0cbb8c9599f6a97..56165e766ba6186489d4e3017b0a7a35510358ab 100644
--- a/cli/src/main/java/hudson/cli/CliPort.java
+++ b/cli/src/main/java/hudson/cli/CliPort.java
@@ -8,9 +8,9 @@ import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
-/**
- * @author Kohsuke Kawaguchi
- */
+ /**
+ * @deprecated Specific to Remoting mode.
+ */
public final class CliPort {
/**
* The TCP endpoint to talk to.
diff --git a/cli/src/main/java/hudson/cli/Connection.java b/cli/src/main/java/hudson/cli/Connection.java
index 1c1ada471fdbaaded6f2203ec130a7e69a671a6f..017051a64a646a76b4ecfe81966381e3874adbca 100644
--- a/cli/src/main/java/hudson/cli/Connection.java
+++ b/cli/src/main/java/hudson/cli/Connection.java
@@ -56,6 +56,9 @@ import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
+/**
+ * Used by Jenkins core only in deprecated Remoting-based CLI.
+ */
public class Connection {
public final InputStream in;
public final OutputStream out;
diff --git a/cli/src/main/java/hudson/cli/DiagnosedStreamCorruptionException.java b/cli/src/main/java/hudson/cli/DiagnosedStreamCorruptionException.java
new file mode 100644
index 0000000000000000000000000000000000000000..4708b425dbb77a715910ac28c67ec84aca79db0f
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/DiagnosedStreamCorruptionException.java
@@ -0,0 +1,55 @@
+package hudson.cli;
+
+import java.io.PrintWriter;
+import java.io.StreamCorruptedException;
+import java.io.StringWriter;
+
+// TODO COPIED FROM hudson.remoting
+
+/**
+ * Signals a {@link StreamCorruptedException} with some additional diagnostic information.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class DiagnosedStreamCorruptionException extends StreamCorruptedException {
+ private final Exception diagnoseFailure;
+ private final byte[] readBack;
+ private final byte[] readAhead;
+
+ DiagnosedStreamCorruptionException(Exception cause, Exception diagnoseFailure, byte[] readBack, byte[] readAhead) {
+ initCause(cause);
+ this.diagnoseFailure = diagnoseFailure;
+ this.readBack = readBack;
+ this.readAhead = readAhead;
+ }
+
+ public Exception getDiagnoseFailure() {
+ return diagnoseFailure;
+ }
+
+ public byte[] getReadBack() {
+ return readBack;
+ }
+
+ public byte[] getReadAhead() {
+ return readAhead;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ buf.append(super.toString()).append("\n");
+ buf.append("Read back: ").append(HexDump.toHex(readBack)).append('\n');
+ buf.append("Read ahead: ").append(HexDump.toHex(readAhead));
+ if (diagnoseFailure!=null) {
+ StringWriter w = new StringWriter();
+ PrintWriter p = new PrintWriter(w);
+ diagnoseFailure.printStackTrace(p);
+ p.flush();
+
+ buf.append("\nDiagnosis problem:\n ");
+ buf.append(w.toString().trim().replace("\n","\n "));
+ }
+ return buf.toString();
+ }
+}
diff --git a/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java b/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java
new file mode 100644
index 0000000000000000000000000000000000000000..ebdd18a192fec4d2e29e25c85dbeb0c2679e40d6
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java
@@ -0,0 +1,191 @@
+package hudson.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+// TODO COPIED FROM hudson.remoting
+
+/**
+ * Filter input stream that records the content as it's read, so that it can be reported
+ * in case of a catastrophic stream corruption problem.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class FlightRecorderInputStream extends InputStream {
+
+ /**
+ * Size (in bytes) of the flight recorder ring buffer used for debugging remoting issues.
+ * @since 2.41
+ */
+ static final int BUFFER_SIZE = Integer.getInteger("hudson.remoting.FlightRecorderInputStream.BUFFER_SIZE", 1024 * 1024);
+
+ private final InputStream source;
+ private ByteArrayRingBuffer recorder = new ByteArrayRingBuffer(BUFFER_SIZE);
+
+ FlightRecorderInputStream(InputStream source) {
+ this.source = source;
+ }
+
+ /**
+ * Rewinds the record buffer and forget everything that was recorded.
+ */
+ public void clear() {
+ recorder = new ByteArrayRingBuffer(BUFFER_SIZE);
+ }
+
+ /**
+ * Gets the recorded content.
+ */
+ public byte[] getRecord() {
+ return recorder.toByteArray();
+ }
+
+ /**
+ * Creates a {@link DiagnosedStreamCorruptionException} based on the recorded content plus read ahead.
+ * The caller is responsible for throwing the exception.
+ */
+ public DiagnosedStreamCorruptionException analyzeCrash(Exception problem, String diagnosisName) {
+ final ByteArrayOutputStream readAhead = new ByteArrayOutputStream();
+ final IOException[] error = new IOException[1];
+
+ Thread diagnosisThread = new Thread(diagnosisName+" stream corruption diagnosis thread") {
+ public void run() {
+ int b;
+ try {
+ // not all InputStream will look for the thread interrupt flag, so check that explicitly to be defensive
+ while (!Thread.interrupted() && (b=source.read())!=-1) {
+ readAhead.write(b);
+ }
+ } catch (IOException e) {
+ error[0] = e;
+ }
+ }
+ };
+
+ // wait up to 1 sec to grab as much data as possible
+ diagnosisThread.start();
+ try {
+ diagnosisThread.join(1000);
+ } catch (InterruptedException ignored) {
+ // we are only waiting for a fixed amount of time, so we'll pretend like we were in a busy loop
+ Thread.currentThread().interrupt();
+ // fall through
+ }
+
+ IOException diagnosisProblem = error[0]; // capture the error, if any, before we kill the thread
+ if (diagnosisThread.isAlive())
+ diagnosisThread.interrupt(); // if it's not dead, kill
+
+ return new DiagnosedStreamCorruptionException(problem,diagnosisProblem,getRecord(),readAhead.toByteArray());
+
+ }
+
+ @Override
+ public int read() throws IOException {
+ int i = source.read();
+ if (i>=0)
+ recorder.write(i);
+ return i;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ len = source.read(b, off, len);
+ if (len>0)
+ recorder.write(b,off,len);
+ return len;
+ }
+
+ /**
+ * To record the bytes we've skipped, convert the call to read.
+ */
+ @Override
+ public long skip(long n) throws IOException {
+ byte[] buf = new byte[(int)Math.min(n,64*1024)];
+ return read(buf,0,buf.length);
+ }
+
+ @Override
+ public int available() throws IOException {
+ return source.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ source.close();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ // http://stackoverflow.com/a/3651696/12916
+ private static class ByteArrayRingBuffer extends OutputStream {
+
+ byte[] data;
+
+ int capacity, pos = 0;
+
+ boolean filled = false;
+
+ public ByteArrayRingBuffer(int capacity) {
+ data = new byte[capacity];
+ this.capacity = capacity;
+ }
+
+ @Override
+ public synchronized void write(int b) {
+ if (pos == capacity) {
+ filled = true;
+ pos = 0;
+ }
+ data[pos++] = (byte) b;
+ }
+
+ public synchronized byte[] toByteArray() {
+ if (!filled) {
+ return Arrays.copyOf(data, pos);
+ }
+ byte[] ret = new byte[capacity];
+ System.arraycopy(data, pos, ret, 0, capacity - pos);
+ System.arraycopy(data, 0, ret, capacity - pos, pos);
+ return ret;
+ }
+
+ /** @author @roadrunner2 */
+ @Override public synchronized void write(byte[] buf, int off, int len) {
+ // no point in trying to copy more than capacity; this also simplifies logic below
+ if (len > capacity) {
+ off += (len - capacity);
+ len = capacity;
+ }
+
+ // copy to buffer, but no farther than the end
+ int num = Math.min(len, capacity - pos);
+ if (num > 0) {
+ System.arraycopy(buf, off, data, pos, num);
+ off += num;
+ len -= num;
+ pos += num;
+ }
+
+ // wrap around if necessary
+ if (pos == capacity) {
+ filled = true;
+ pos = 0;
+ }
+
+ // copy anything still left
+ if (len > 0) {
+ System.arraycopy(buf, off, data, pos, len);
+ pos += len;
+ }
+ }
+
+ }
+
+}
diff --git a/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java b/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
index 3c7911e1723d0191340dcd457af46a23fbb5a65e..c01f3261ceba3cefcaa1cab4a759daefc1a78e90 100644
--- a/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
+++ b/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
@@ -20,7 +20,7 @@ import org.apache.commons.codec.binary.Base64;
* @author Kohsuke Kawaguchi
*/
public class FullDuplexHttpStream {
- private final URL target;
+ private final URL base;
/**
* Authorization header value needed to get through the HTTP layer.
*/
@@ -49,15 +49,36 @@ public class FullDuplexHttpStream {
}
/**
+ * @param target something like {@code http://jenkins/cli?remoting=true}
+ * which we then need to split into {@code http://jenkins/} + {@code cli?remoting=true}
+ * in order to construct a crumb issuer request
+ * @deprecated use {@link #FullDuplexHttpStream(URL, String, String)} instead
+ */
+ @Deprecated
+ public FullDuplexHttpStream(URL target, String authorization) throws IOException {
+ this(new URL(target.toString().replaceFirst("/cli.*$", "/")), target.toString().replaceFirst("^.+/(cli.*)$", "$1"), authorization);
+ }
+
+ /**
+ * @param base the base URL of Jenkins
* @param target
* The endpoint that we are making requests to.
* @param authorization
* The value of the authorization header, if non-null.
*/
- public FullDuplexHttpStream(URL target, String authorization) throws IOException {
- this.target = target;
+ public FullDuplexHttpStream(URL base, String relativeTarget, String authorization) throws IOException {
+ if (!base.toString().endsWith("/")) {
+ throw new IllegalArgumentException(base.toString());
+ }
+ if (relativeTarget.startsWith("/")) {
+ throw new IllegalArgumentException(relativeTarget);
+ }
+
+ this.base = base;
this.authorization = authorization;
+ URL target = new URL(base, relativeTarget);
+
CrumbData crumbData = new CrumbData();
UUID uuid = UUID.randomUUID(); // so that the server can correlate those two connections
@@ -77,8 +98,9 @@ public class FullDuplexHttpStream {
con.getOutputStream().close();
input = con.getInputStream();
// make sure we hit the right URL
- if(con.getHeaderField("Hudson-Duplex")==null)
- throw new IOException(target+" doesn't look like Jenkins");
+ if (con.getHeaderField("Hudson-Duplex") == null) {
+ throw new IOException(target + " does not look like Jenkins, or is not serving the HTTP Duplex transport");
+ }
// client->server uses chunked encoded POST for unlimited capacity.
con = (HttpURLConnection) target.openConnection();
@@ -128,8 +150,7 @@ public class FullDuplexHttpStream {
}
private String createCrumbUrlBase() {
- String url = target.toExternalForm();
- return new StringBuilder(url.substring(0, url.lastIndexOf("/cli"))).append("/crumbIssuer/api/xml/").toString();
+ return base + "crumbIssuer/api/xml/";
}
private String readData(String dest) throws IOException {
diff --git a/cli/src/main/java/hudson/cli/HexDump.java b/cli/src/main/java/hudson/cli/HexDump.java
new file mode 100644
index 0000000000000000000000000000000000000000..ad37158bc16d0e7939a34831bd7628120eb61207
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/HexDump.java
@@ -0,0 +1,47 @@
+package hudson.cli;
+
+// TODO COPIED FROM hudson.remoting
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+class HexDump {
+ private static final String CODE = "0123456789abcdef";
+
+ public static String toHex(byte[] buf) {
+ return toHex(buf,0,buf.length);
+ }
+ public static String toHex(byte[] buf, int start, int len) {
+ StringBuilder r = new StringBuilder(len*2);
+ boolean inText = false;
+ for (int i=0; i= 0x20 && b <= 0x7e) {
+ if (!inText) {
+ inText = true;
+ r.append('\'');
+ }
+ r.append((char) b);
+ } else {
+ if (inText) {
+ r.append("' ");
+ inText = false;
+ }
+ r.append("0x");
+ r.append(CODE.charAt((b>>4)&15));
+ r.append(CODE.charAt(b&15));
+ if (i < len - 1) {
+ if (b == 10) {
+ r.append('\n');
+ } else {
+ r.append(' ');
+ }
+ }
+ }
+ }
+ if (inText) {
+ r.append('\'');
+ }
+ return r.toString();
+ }
+}
diff --git a/cli/src/main/java/hudson/cli/PlainCLIProtocol.java b/cli/src/main/java/hudson/cli/PlainCLIProtocol.java
new file mode 100644
index 0000000000000000000000000000000000000000..17d232c87b570ab0d80069ed926a54613429451f
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/PlainCLIProtocol.java
@@ -0,0 +1,354 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.ReadPendingException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.CountingInputStream;
+
+/**
+ * CLI protocol working over a plain socket-like connection, without SSH or Remoting.
+ * Each side consists of frames starting with an {@code int} length,
+ * then a {@code byte} opcode, then any opcode-specific data.
+ * The length does not count the length field itself nor the opcode, so it is nonnegative.
+ */
+class PlainCLIProtocol {
+
+ static final Logger LOGGER = Logger.getLogger(PlainCLIProtocol.class.getName());
+
+ /** One-byte operation to send to the other side. */
+ private enum Op {
+ /** UTF-8 command name or argument. */
+ ARG(true),
+ /** UTF-8 locale identifier. */
+ LOCALE(true),
+ /** UTF-8 client encoding. */
+ ENCODING(true),
+ /** Start running command. */
+ START(true),
+ /** Exit code, as int. */
+ EXIT(false),
+ /** Chunk of stdin, as int length followed by bytes. */
+ STDIN(true),
+ /** EOF on stdin. */
+ END_STDIN(true),
+ /** Chunk of stdout. */
+ STDOUT(false),
+ /** Chunk of stderr. */
+ STDERR(false);
+ /** True if sent from the client to the server; false if sent from the server to the client. */
+ final boolean clientSide;
+ Op(boolean clientSide) {
+ this.clientSide = clientSide;
+ }
+ }
+
+ static abstract class EitherSide implements Closeable {
+
+ private final CountingInputStream cis;
+ private final FlightRecorderInputStream flightRecorder;
+ final DataInputStream dis;
+ final DataOutputStream dos;
+
+ protected EitherSide(InputStream is, OutputStream os) {
+ cis = new CountingInputStream(is);
+ flightRecorder = new FlightRecorderInputStream(cis);
+ dis = new DataInputStream(flightRecorder);
+ dos = new DataOutputStream(os);
+ }
+
+ final void begin() {
+ new Reader().start();
+ }
+
+ private class Reader extends Thread {
+
+ Reader() {
+ super("PlainCLIProtocol"); // TODO set distinctive Thread.name
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (true) {
+ LOGGER.finest("reading frame");
+ int framelen;
+ try {
+ framelen = dis.readInt();
+ } catch (EOFException x) {
+ handleClose();
+ break; // TODO verify that we hit EOF immediately, not partway into framelen
+ } catch (IOException x) {
+ if (x.getCause() instanceof TimeoutException) { // TODO on Tomcat this seems to be SocketTimeoutException
+ LOGGER.log(Level.FINE, "ignoring idle timeout, perhaps from Jetty", x);
+ continue;
+ } else {
+ throw x;
+ }
+ }
+ if (framelen < 0) {
+ throw new IOException("corrupt stream: negative frame length");
+ }
+ byte b = dis.readByte();
+ if (b < 0) { // i.e., >127
+ throw new IOException("corrupt stream: negative operation code");
+ }
+ if (b >= Op.values().length) {
+ LOGGER.log(Level.WARNING, "unknown operation #{0}: {1}", new Object[] {b, HexDump.toHex(flightRecorder.getRecord())});
+ IOUtils.skipFully(dis, framelen);
+ continue;
+ }
+ Op op = Op.values()[b];
+ long start = cis.getByteCount();
+ LOGGER.log(Level.FINEST, "handling frame with {0} of length {1}", new Object[] {op, framelen});
+ boolean handled = handle(op, framelen);
+ if (handled) {
+ long actuallyRead = cis.getByteCount() - start;
+ if (actuallyRead != framelen) {
+ throw new IOException("corrupt stream: expected to read " + framelen + " bytes from " + op + " but read " + actuallyRead);
+ }
+ } else {
+ LOGGER.log(Level.WARNING, "unexpected {0}: {1}", new Object[] {op, HexDump.toHex(flightRecorder.getRecord())});
+ IOUtils.skipFully(dis, framelen);
+ }
+ }
+ } catch (ClosedChannelException x) {
+ LOGGER.log(Level.FINE, null, x);
+ handleClose();
+ } catch (IOException x) {
+ LOGGER.log(Level.WARNING, null, flightRecorder.analyzeCrash(x, "broken stream"));
+ } catch (ReadPendingException x) {
+ // in case trick in CLIAction does not work
+ LOGGER.log(Level.FINE, null, x);
+ handleClose();
+ } catch (RuntimeException x) {
+ LOGGER.log(Level.WARNING, null, x);
+ handleClose();
+ }
+ }
+
+ }
+
+ protected abstract void handleClose();
+
+ protected abstract boolean handle(Op op, int framelen) throws IOException;
+
+ private void writeOp(Op op) throws IOException {
+ dos.writeByte((byte) op.ordinal());
+ }
+
+ protected final synchronized void send(Op op) throws IOException {
+ dos.writeInt(0);
+ writeOp(op);
+ dos.flush();
+ }
+
+ protected final synchronized void send(Op op, int number) throws IOException {
+ dos.writeInt(4);
+ writeOp(op);
+ dos.writeInt(number);
+ dos.flush();
+ }
+
+ protected final synchronized void send(Op op, byte[] chunk, int off, int len) throws IOException {
+ dos.writeInt(len);
+ writeOp(op);
+ dos.write(chunk, off, len);
+ dos.flush();
+ }
+
+ protected final void send(Op op, byte[] chunk) throws IOException {
+ send(op, chunk, 0, chunk.length);
+ }
+
+ protected final void send(Op op, String text) throws IOException {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ new DataOutputStream(buf).writeUTF(text);
+ send(op, buf.toByteArray());
+ }
+
+ protected final byte[] readChunk(int framelen) throws IOException {
+ assert Thread.currentThread() instanceof EitherSide.Reader;
+ byte[] buf = new byte[framelen];
+ dis.readFully(buf);
+ return buf;
+ }
+
+ protected final OutputStream stream(final Op op) {
+ return new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ send(op, new byte[] {(byte) b});
+ }
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ send(op, b, off, len);
+ }
+ @Override
+ public void write(byte[] b) throws IOException {
+ send(op, b);
+ }
+ };
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ dos.close();
+ }
+
+ }
+
+ static abstract class ServerSide extends EitherSide {
+
+ ServerSide(InputStream is, OutputStream os) {
+ super(is, os);
+ }
+
+ @Override
+ protected final boolean handle(Op op, int framelen) throws IOException {
+ assert Thread.currentThread() instanceof EitherSide.Reader;
+ assert op.clientSide;
+ switch (op) {
+ case ARG:
+ onArg(dis.readUTF());
+ return true;
+ case LOCALE:
+ onLocale(dis.readUTF());
+ return true;
+ case ENCODING:
+ onEncoding(dis.readUTF());
+ return true;
+ case START:
+ onStart();
+ return true;
+ case STDIN:
+ onStdin(readChunk(framelen));
+ return true;
+ case END_STDIN:
+ onEndStdin();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ protected abstract void onArg(String text);
+
+ protected abstract void onLocale(String text);
+
+ protected abstract void onEncoding(String text);
+
+ protected abstract void onStart();
+
+ protected abstract void onStdin(byte[] chunk) throws IOException;
+
+ protected abstract void onEndStdin() throws IOException;
+
+ public final void sendExit(int code) throws IOException {
+ send(Op.EXIT, code);
+ }
+
+ public final OutputStream streamStdout() {
+ return stream(Op.STDOUT);
+ }
+
+ public final OutputStream streamStderr() {
+ return stream(Op.STDERR);
+ }
+
+ }
+
+ static abstract class ClientSide extends EitherSide {
+
+ ClientSide(InputStream is, OutputStream os) {
+ super(is, os);
+ }
+
+ @Override
+ protected boolean handle(Op op, int framelen) throws IOException {
+ assert Thread.currentThread() instanceof EitherSide.Reader;
+ assert !op.clientSide;
+ switch (op) {
+ case EXIT:
+ onExit(dis.readInt());
+ return true;
+ case STDOUT:
+ onStdout(readChunk(framelen));
+ return true;
+ case STDERR:
+ onStderr(readChunk(framelen));
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ protected abstract void onExit(int code);
+
+ protected abstract void onStdout(byte[] chunk) throws IOException;
+
+ protected abstract void onStderr(byte[] chunk) throws IOException;
+
+ public final void sendArg(String text) throws IOException {
+ send(Op.ARG, text);
+ }
+
+ public final void sendLocale(String text) throws IOException {
+ send(Op.LOCALE, text);
+ }
+
+ public final void sendEncoding(String text) throws IOException {
+ send(Op.ENCODING, text);
+ }
+
+ public final void sendStart() throws IOException {
+ send(Op.START);
+ }
+
+ public final OutputStream streamStdin() {
+ return stream(Op.STDIN);
+ }
+
+ public final void sendEndStdin() throws IOException {
+ send(Op.END_STDIN);
+ }
+
+ }
+
+ private PlainCLIProtocol() {}
+
+}
diff --git a/cli/src/main/java/hudson/cli/SSHCLI.java b/cli/src/main/java/hudson/cli/SSHCLI.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a2ecc4f388562f740e4c6a4310d9dd0ee6c4c0d
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/SSHCLI.java
@@ -0,0 +1,132 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.cli;
+
+import hudson.util.QuotedStringTokenizer;
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import static java.util.logging.Level.FINE;
+import java.util.logging.Logger;
+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.SecurityUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseOutputStream;
+
+/**
+ * Implements SSH connection mode of {@link CLI}.
+ * In a separate class to avoid any class loading of {@code sshd-core} when not using {@code -ssh} mode.
+ * That allows the {@code test} module to pick up a version of {@code sshd-core} from the {@code sshd} module via {@code jenkins-war}
+ * that may not match the version being used from the {@code cli} module and may not be compatible.
+ */
+class SSHCLI {
+
+ static int sshConnection(String jenkinsUrl, String user, List args, PrivateKeyProvider provider, final boolean strictHostKey) throws IOException {
+ Logger.getLogger(SecurityUtils.class.getName()).setLevel(Level.WARNING); // suppress: BouncyCastle not registered, using the default JCE provider
+ URL url = new URL(jenkinsUrl + "login");
+ URLConnection conn = url.openConnection();
+ String endpointDescription = conn.getHeaderField("X-SSH-Endpoint");
+
+ if (endpointDescription == null) {
+ CLI.LOGGER.warning("No header 'X-SSH-Endpoint' returned by Jenkins");
+ return -1;
+ }
+
+ CLI.LOGGER.log(FINE, "Connecting via SSH to: {0}", endpointDescription);
+
+ int sshPort = Integer.parseInt(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) {
+ CLI.LOGGER.log(Level.WARNING, "Unknown host key for {0}", remoteAddress.toString());
+ return !strictHostKey;
+ }
+ }, 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()) {
+ CLI.LOGGER.log(FINE, "Offering {0} private key", pair.getPrivate().getAlgorithm());
+ 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 SSHCLI() {}
+
+}
diff --git a/cli/src/main/java/hudson/cli/SequenceOutputStream.java b/cli/src/main/java/hudson/cli/SequenceOutputStream.java
deleted file mode 100644
index fb6c26523b47317af89a975c7bd67930095ccabc..0000000000000000000000000000000000000000
--- a/cli/src/main/java/hudson/cli/SequenceOutputStream.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package hudson.cli;
-
-import java.io.OutputStream;
-import java.io.IOException;
-import java.io.SequenceInputStream;
-
-/**
- * {@link OutputStream} version of {@link SequenceInputStream}.
- *
- * Provides a single {@link OutputStream} view over multiple {@link OutputStream}s (each of the fixed length.)
- *
- * @author Kohsuke Kawaguchi
- */
-abstract class SequenceOutputStream extends OutputStream {
- protected static class Block {
- final OutputStream out;
- long capacity;
-
- public Block(OutputStream out, long capacity) {
- this.out = out;
- this.capacity = capacity;
- }
- }
-
- /**
- * Current block being written.
- */
- private Block block;
-
- protected SequenceOutputStream(Block block) {
- this.block = block;
- }
-
- @Override
- public void write(byte[] b, int off, int len) throws IOException {
- while(len>0) {
- int sz = (int)Math.min(len, block.capacity);
- block.out.write(b,off,sz);
- block.capacity -=sz;
- len-=sz;
- off+=sz;
- swapIfNeeded();
- }
- }
-
- public void write(int b) throws IOException {
- block.out.write(b);
- block.capacity--;
- swapIfNeeded();
- }
-
- private void swapIfNeeded() throws IOException {
- if(block.capacity >0) return;
- block.out.close();
- block=next(block);
- }
-
- @Override
- public void flush() throws IOException {
- block.out.flush();
- }
-
- @Override
- public void close() throws IOException {
- block.out.close();
- block=null;
- }
-
- /**
- * Fetches the next {@link OutputStream} to write to,
- * along with their capacity.
- */
- protected abstract Block next(Block current) throws IOException;
-}
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 1c091fbfb0d20ce33432ee40dd0f356c40f236c1..921fe67a211da560ac75fc355179a117b3c00dc2 100644
--- a/cli/src/main/resources/hudson/cli/client/Messages.properties
+++ b/cli/src/main/resources/hudson/cli/client/Messages.properties
@@ -2,11 +2,18 @@ CLI.Usage=Jenkins CLI\n\
Usage: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
Options:\n\
-s URL : the server URL (defaults to the JENKINS_URL env var)\n\
- -i KEY : SSH private key file used for authentication\n\
+ -http : use a plain CLI protocol over HTTP(S) (the default; mutually exclusive with -ssh and -remoting)\n\
+ -ssh : use SSH protocol (requires -user; SSH port must be open on server, and user must have registered a public key)\n\
+ -remoting : use deprecated Remoting channel protocol (if enabled on server; for compatibility with legacy commands or command modes only)\n\
+ -i KEY : SSH private key file used for authentication (for use with -ssh or -remoting)\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\
- -v : verbose output. Display logs on the console by setting j.u.l log level to FINEST\n\
+ -user : specify user (for use with -ssh)\n\
+ -strictHostKey : request strict host key checking (for use with -ssh)\n\
+ -logger FINE : enable detailed logging from the client\n\
+ -auth [ USER:SECRET | @FILE ] : specify username and either password or API token (or load from them both from a file);\n\
+ for use with -http, or -remoting but only when the JNLP agent port is disabled\n\
\n\
The available commands depend on the server. Run the 'help' command to\n\
see the list.
diff --git a/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java b/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4663fbafd5e0aa1a05852562ef8c8560f3d0cac3
--- /dev/null
+++ b/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java
@@ -0,0 +1,132 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import static org.junit.Assert.*;
+import org.junit.Test;
+
+public class PlainCLIProtocolTest {
+
+ @Test
+ public void ignoreUnknownOperations() throws Exception {
+ final PipedOutputStream upload = new PipedOutputStream();
+ final PipedOutputStream download = new PipedOutputStream();
+ class Client extends PlainCLIProtocol.ClientSide {
+ int code = -1;
+ final ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+ Client() throws IOException {
+ super(new PipedInputStream(download), upload);
+ }
+ @Override
+ protected synchronized void onExit(int code) {
+ this.code = code;
+ notifyAll();
+ }
+ @Override
+ protected void onStdout(byte[] chunk) throws IOException {
+ stdout.write(chunk);
+ }
+ @Override
+ protected void onStderr(byte[] chunk) throws IOException {}
+ @Override
+ protected void handleClose() {}
+ void send() throws IOException {
+ sendArg("command");
+ sendStart();
+ streamStdin().write("hello".getBytes());
+ }
+ void newop() throws IOException {
+ dos.writeInt(0);
+ dos.writeByte(99);
+ dos.flush();
+ }
+ }
+ class Server extends PlainCLIProtocol.ServerSide {
+ String arg;
+ boolean started;
+ final ByteArrayOutputStream stdin = new ByteArrayOutputStream();
+ Server() throws IOException {
+ super(new PipedInputStream(upload), download);
+ }
+ @Override
+ protected void onArg(String text) {
+ arg = text;
+ }
+ @Override
+ protected void onLocale(String text) {}
+ @Override
+ protected void onEncoding(String text) {}
+ @Override
+ protected synchronized void onStart() {
+ started = true;
+ notifyAll();
+ }
+ @Override
+ protected void onStdin(byte[] chunk) throws IOException {
+ stdin.write(chunk);
+ }
+ @Override
+ protected void onEndStdin() throws IOException {}
+ @Override
+ protected void handleClose() {}
+ void send() throws IOException {
+ streamStdout().write("goodbye".getBytes());
+ sendExit(2);
+ }
+ void newop() throws IOException {
+ dos.writeInt(0);
+ dos.writeByte(99);
+ dos.flush();
+ }
+ }
+ Client client = new Client();
+ Server server = new Server();
+ client.begin();
+ server.begin();
+ client.send();
+ client.newop();
+ synchronized (server) {
+ while (!server.started) {
+ server.wait();
+ }
+ }
+ server.newop();
+ server.send();
+ synchronized (client) {
+ while (client.code == -1) {
+ client.wait();
+ }
+ }
+ assertEquals("hello", server.stdin.toString());
+ assertEquals("command", server.arg);
+ assertEquals("goodbye", client.stdout.toString());
+ assertEquals(2, client.code);
+ }
+
+}
diff --git a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java
index 376b1fa815f3f62988f741c93a63f2f2d090563a..86970265089b4bf41cb7bc4aae7d6e320447d21a 100644
--- a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java
+++ b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java
@@ -57,7 +57,7 @@ public class PrivateKeyProviderTest {
final File dsaKey = keyFile(".ssh/id_dsa");
final File rsaKey = keyFile(".ssh/id_rsa");
- run("-i", dsaKey.getAbsolutePath(), "-i", rsaKey.getAbsolutePath(), "-s", "http://example.com");
+ run("-remoting", "-i", dsaKey.getAbsolutePath(), "-i", rsaKey.getAbsolutePath(), "-s", "http://example.com");
verify(cli).authenticate(withKeyPairs(
keyPair(dsaKey),
@@ -73,7 +73,7 @@ public class PrivateKeyProviderTest {
final File dsaKey = keyFile(".ssh/id_dsa");
fakeHome();
- run("-s", "http://example.com");
+ run("-remoting", "-s", "http://example.com");
verify(cli).authenticate(withKeyPairs(
keyPair(rsaKey),
diff --git a/core/pom.xml b/core/pom.xml
index f8d7e4eb465cde17dd55fe9283a8fd8a413dc77b..c9d2cee765292af3d31d58dced46318b21094046 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -772,7 +772,6 @@ THE SOFTWARE.
0.5C
true
-noverify
- false
diff --git a/core/src/main/java/hudson/cli/CLIAction.java b/core/src/main/java/hudson/cli/CLIAction.java
index 0053c278bc3ab5333f81f55470c3f9592244e774..2c37db46cc0ed3e52184fe7d9ef9cb264bbca21b 100644
--- a/core/src/main/java/hudson/cli/CLIAction.java
+++ b/core/src/main/java/hudson/cli/CLIAction.java
@@ -37,7 +37,6 @@ import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
-import org.kohsuke.stapler.HttpResponses.HttpResponseException;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
@@ -46,6 +45,21 @@ import org.kohsuke.stapler.StaplerResponse;
import hudson.Extension;
import hudson.model.FullDuplexHttpChannel;
import hudson.remoting.Channel;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.util.FullDuplexHttpService;
+import org.kohsuke.stapler.HttpResponses;
/**
* Shows usage of CLI and commands.
@@ -56,7 +70,9 @@ import hudson.remoting.Channel;
@Restricted(NoExternalUse.class)
public class CLIAction implements UnprotectedRootAction, StaplerProxy {
- private transient final Map duplexChannels = new HashMap();
+ private static final Logger LOGGER = Logger.getLogger(CLIAction.class.getName());
+
+ private transient final Map duplexServices = new HashMap<>();
public String getIconFileName() {
return null;
@@ -67,7 +83,7 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy {
}
public String getUrlName() {
- return jenkins.CLI.DISABLED ? null : "cli";
+ return "cli";
}
public void doCommand(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException {
@@ -91,46 +107,159 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy {
StaplerRequest req = Stapler.getCurrentRequest();
if (req.getRestOfPath().length()==0 && "POST".equals(req.getMethod())) {
// CLI connection request
- throw new CliEndpointResponse();
+ if ("false".equals(req.getParameter("remoting"))) {
+ throw new PlainCliEndpointResponse();
+ } else if (jenkins.CLI.get().isEnabled()) {
+ throw new RemotingCliEndpointResponse();
+ } else {
+ throw HttpResponses.forbidden();
+ }
} else {
return this;
}
}
/**
- * Serves CLI-over-HTTP response.
+ * Serves {@link PlainCLIProtocol} response.
*/
- private class CliEndpointResponse extends HttpResponseException {
- @Override
- public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
- try {
- // do not require any permission to establish a CLI connection
- // the actual authentication for the connecting Channel is done by CLICommand
+ private class PlainCliEndpointResponse extends FullDuplexHttpService.Response {
- UUID uuid = UUID.fromString(req.getHeader("Session"));
- rsp.setHeader("Hudson-Duplex",""); // set the header so that the client would know
+ PlainCliEndpointResponse() {
+ super(duplexServices);
+ }
- FullDuplexHttpChannel server;
- if(req.getHeader("Side").equals("download")) {
- duplexChannels.put(uuid,server=new FullDuplexHttpChannel(uuid, !Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) {
+ @Override
+ protected FullDuplexHttpService createService(StaplerRequest req, UUID uuid) throws IOException {
+ return new FullDuplexHttpService(uuid) {
+ @Override
+ protected void run(InputStream upload, OutputStream download) throws IOException, InterruptedException {
+ final AtomicReference runningThread = new AtomicReference<>();
+ class ServerSideImpl extends PlainCLIProtocol.ServerSide {
+ boolean ready;
+ List args = new ArrayList<>();
+ Locale locale = Locale.getDefault();
+ Charset encoding = Charset.defaultCharset();
+ final PipedInputStream stdin = new PipedInputStream();
+ final PipedOutputStream stdinMatch = new PipedOutputStream();
+ ServerSideImpl(InputStream is, OutputStream os) throws IOException {
+ super(is, os);
+ stdinMatch.connect(stdin);
+ }
+ @Override
+ protected void onArg(String text) {
+ args.add(text);
+ }
+ @Override
+ protected void onLocale(String text) {
+ for (Locale _locale : Locale.getAvailableLocales()) {
+ if (_locale.toString().equals(text)) {
+ locale = _locale;
+ return;
+ }
+ }
+ LOGGER.log(Level.WARNING, "unknown client locale {0}", text);
+ }
@Override
- protected void main(Channel channel) throws IOException, InterruptedException {
- // capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator()
- channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, Jenkins.getAuthentication());
- channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel));
+ protected void onEncoding(String text) {
+ try {
+ encoding = Charset.forName(text);
+ } catch (UnsupportedCharsetException x) {
+ LOGGER.log(Level.WARNING, "unknown client charset {0}", text);
+ }
+ }
+ @Override
+ protected void onStart() {
+ ready();
+ }
+ @Override
+ protected void onStdin(byte[] chunk) throws IOException {
+ stdinMatch.write(chunk);
+ }
+ @Override
+ protected void onEndStdin() throws IOException {
+ stdinMatch.close();
+ }
+ @Override
+ protected void handleClose() {
+ ready();
+ Thread t = runningThread.get();
+ if (t != null) {
+ t.interrupt();
+ }
+ }
+ private synchronized void ready() {
+ ready = true;
+ notifyAll();
+ }
+ }
+ try (ServerSideImpl connection = new ServerSideImpl(upload, download)) {
+ connection.begin();
+ synchronized (connection) {
+ while (!connection.ready) {
+ connection.wait();
+ }
+ }
+ PrintStream stdout = new PrintStream(connection.streamStdout(), false, connection.encoding.name());
+ PrintStream stderr = new PrintStream(connection.streamStderr(), true, connection.encoding.name());
+ if (connection.args.isEmpty()) {
+ stderr.println("Connection closed before arguments received");
+ connection.sendExit(2);
+ return;
+ }
+ String commandName = connection.args.get(0);
+ CLICommand command = CLICommand.clone(commandName);
+ if (command == null) {
+ stderr.println("No such command " + commandName);
+ connection.sendExit(2);
+ return;
+ }
+ command.setTransportAuth(Jenkins.getAuthentication());
+ command.setClientCharset(connection.encoding);
+ CLICommand orig = CLICommand.setCurrent(command);
+ try {
+ runningThread.set(Thread.currentThread());
+ int exit = command.main(connection.args.subList(1, connection.args.size()), connection.locale, connection.stdin, stdout, stderr);
+ stdout.flush();
+ connection.sendExit(exit);
+ try { // seems to avoid ReadPendingException from Jetty
+ Thread.sleep(1000);
+ } catch (InterruptedException x) {
+ // expected; ignore
+ }
+ } finally {
+ CLICommand.setCurrent(orig);
+ runningThread.set(null);
}
- });
- try {
- server.download(req,rsp);
- } finally {
- duplexChannels.remove(uuid);
}
- } else {
- duplexChannels.get(uuid).upload(req,rsp);
}
- } catch (InterruptedException e) {
- throw new IOException(e);
- }
+ };
+ }
+ }
+
+ /**
+ * Serves Remoting-over-HTTP response.
+ */
+ private class RemotingCliEndpointResponse extends FullDuplexHttpService.Response {
+
+ RemotingCliEndpointResponse() {
+ super(duplexServices);
+ }
+
+ @Override
+ protected FullDuplexHttpService createService(StaplerRequest req, UUID uuid) throws IOException {
+ // do not require any permission to establish a CLI connection
+ // the actual authentication for the connecting Channel is done by CLICommand
+
+ return new FullDuplexHttpChannel(uuid, !Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void main(Channel channel) throws IOException, InterruptedException {
+ // capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator()
+ channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, Jenkins.getAuthentication());
+ channel.setProperty(CliEntryPoint.class.getName(), new CliManagerImpl(channel));
+ }
+ };
}
}
+
}
diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java
index bc5d2ea4c5d84de24b88239f4e4e9759569980e4..d507662037b575cc82fab21ed0fcb21c9e764850 100644
--- a/core/src/main/java/hudson/cli/CLICommand.java
+++ b/core/src/main/java/hudson/cli/CLICommand.java
@@ -71,6 +71,8 @@ import java.util.Locale;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
/**
* Base class for Hudson CLI.
@@ -151,7 +153,9 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
*
* See {@link #checkChannel()} to get a channel and throw an user-friendly
* exception
+ * @deprecated Specific to Remoting-based protocol.
*/
+ @Deprecated
public transient Channel channel;
/**
@@ -159,6 +163,11 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
*/
public transient Locale locale;
+ /**
+ * The encoding of the client, if defined.
+ */
+ private transient @CheckForNull Charset encoding;
+
/**
* Set by the caller of the CLI system if the transport already provides
* authentication. Due to the compatibility issue, we still allow the user
@@ -316,17 +325,23 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
protected CmdLineParser getCmdLineParser() {
return new CmdLineParser(this);
}
-
+
+ /**
+ * @deprecated Specific to Remoting-based protocol.
+ */
+ @Deprecated
public Channel checkChannel() throws AbortException {
if (channel==null)
- throw new AbortException("This command can only run with Jenkins CLI. See https://jenkins.io/redirect/cli-command-requires-channel");
+ throw new AbortException("This command is requesting the deprecated -remoting mode. See https://jenkins.io/redirect/cli-command-requires-channel");
return channel;
}
/**
* Loads the persisted authentication information from {@link ClientAuthenticationCache}
* if the current transport provides {@link Channel}.
+ * @deprecated Assumes Remoting, and vulnerable to JENKINS-12543.
*/
+ @Deprecated
protected Authentication loadStoredAuthentication() throws InterruptedException {
try {
if (channel!=null)
@@ -353,7 +368,9 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
* Always non-null.
* If the underlying transport had already performed authentication, this object is something other than
* {@link jenkins.model.Jenkins#ANONYMOUS}.
+ * @deprecated Unused.
*/
+ @Deprecated
protected boolean shouldPerformAuthentication(Authentication auth) {
return auth== Jenkins.ANONYMOUS;
}
@@ -463,7 +480,9 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
/**
* Convenience method for subtypes to obtain the system property of the client.
+ * @deprecated Specific to Remoting-based protocol.
*/
+ @Deprecated
protected String getClientSystemProperty(String name) throws IOException, InterruptedException {
return checkChannel().call(new GetSystemProperty(name));
}
@@ -482,7 +501,18 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
private static final long serialVersionUID = 1L;
}
- protected Charset getClientCharset() throws IOException, InterruptedException {
+ /**
+ * Define the encoding for the command.
+ * @since 2.54
+ */
+ public void setClientCharset(@Nonnull Charset encoding) {
+ this.encoding = encoding;
+ }
+
+ protected @Nonnull Charset getClientCharset() throws IOException, InterruptedException {
+ if (encoding != null) {
+ return encoding;
+ }
if (channel==null)
// for SSH, assume the platform default encoding
// this is in-line with the standard SSH behavior
@@ -507,7 +537,9 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
/**
* Convenience method for subtypes to obtain environment variables of the client.
+ * @deprecated Specific to Remoting-based protocol.
*/
+ @Deprecated
protected String getClientEnvironmentVariable(String name) throws IOException, InterruptedException {
return checkChannel().call(new GetEnvironmentVariable(name));
}
diff --git a/core/src/main/java/hudson/cli/CliManagerImpl.java b/core/src/main/java/hudson/cli/CliManagerImpl.java
index 577cf3ce132fe373b6f95e80c95ff2415e75e366..750a38d210af7cb8f267086346715b3981129b50 100644
--- a/core/src/main/java/hudson/cli/CliManagerImpl.java
+++ b/core/src/main/java/hudson/cli/CliManagerImpl.java
@@ -44,7 +44,9 @@ import java.util.logging.Logger;
* {@link CliEntryPoint} implementation exposed to the remote CLI.
*
* @author Kohsuke Kawaguchi
+ * @deprecated Specific to Remoting-based protocol.
*/
+@Deprecated
public class CliManagerImpl implements CliEntryPoint, Serializable {
private transient final Channel channel;
diff --git a/core/src/main/java/hudson/cli/CliProtocol.java b/core/src/main/java/hudson/cli/CliProtocol.java
index d663957fe9debe8998bb698ece5067fa40b410bd..cdf25b033ff048d8bb6b5696253a3ef4afcc4830 100644
--- a/core/src/main/java/hudson/cli/CliProtocol.java
+++ b/core/src/main/java/hudson/cli/CliProtocol.java
@@ -26,7 +26,9 @@ import java.net.Socket;
*
* @author Kohsuke Kawaguchi
* @since 1.467
+ * @deprecated Implementing Remoting-based protocol.
*/
+@Deprecated
@Extension @Symbol("cli")
public class CliProtocol extends AgentProtocol {
@Inject
@@ -42,7 +44,7 @@ public class CliProtocol extends AgentProtocol {
@Override
public String getName() {
- return jenkins.CLI.DISABLED ? null : "CLI-connect";
+ return jenkins.CLI.get().isEnabled() ? "CLI-connect" : null;
}
/**
diff --git a/core/src/main/java/hudson/cli/CliProtocol2.java b/core/src/main/java/hudson/cli/CliProtocol2.java
index bb14599dbfea037d7d07517d828ffb9aa7f968b9..e7d0bad71fa4ccc152bc25a7a2eaaefe3d958101 100644
--- a/core/src/main/java/hudson/cli/CliProtocol2.java
+++ b/core/src/main/java/hudson/cli/CliProtocol2.java
@@ -20,12 +20,14 @@ import java.security.Signature;
*
* @author Kohsuke Kawaguchi
* @since 1.467
+ * @deprecated Implementing Remoting-based protocol.
*/
+@Deprecated
@Extension @Symbol("cli2")
public class CliProtocol2 extends CliProtocol {
@Override
public String getName() {
- return jenkins.CLI.DISABLED ? null : "CLI2-connect";
+ return jenkins.CLI.get().isEnabled() ? "CLI2-connect" : null;
}
/**
diff --git a/core/src/main/java/hudson/cli/CliTransportAuthenticator.java b/core/src/main/java/hudson/cli/CliTransportAuthenticator.java
index f561a50eddf21c362f130fdf1c445f8003ceaa62..327a4e2707a1fd760ada27a3dbbd667fc8e29b36 100644
--- a/core/src/main/java/hudson/cli/CliTransportAuthenticator.java
+++ b/core/src/main/java/hudson/cli/CliTransportAuthenticator.java
@@ -20,7 +20,9 @@ import hudson.security.SecurityRealm;
*
* @author Kohsuke Kawaguchi
* @since 1.419
+ * @deprecated Specific to Remoting-based protocol.
*/
+@Deprecated
public abstract class CliTransportAuthenticator implements ExtensionPoint {
/**
* Checks if this implementation supports the specified protocol.
diff --git a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java
index fb754417861c61c3e17b3c45ae66572d4f25b1c9..16f11eb0ba5382769dd40d45b67fa4e7e50498f9 100644
--- a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java
+++ b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java
@@ -27,7 +27,9 @@ import java.util.Properties;
*
* @author Kohsuke Kawaguchi
* @since 1.351
+ * @deprecated Assumes Remoting, and vulnerable to JENKINS-12543.
*/
+@Deprecated
public class ClientAuthenticationCache implements Serializable {
/**
* Where the store should be placed.
diff --git a/core/src/main/java/hudson/cli/CommandDuringBuild.java b/core/src/main/java/hudson/cli/CommandDuringBuild.java
index 67e7d64717f6377d1522c561e8d676bb53c707a1..17d154d6fa3531a9ec7bd3df8a92c0dbe7d132a9 100644
--- a/core/src/main/java/hudson/cli/CommandDuringBuild.java
+++ b/core/src/main/java/hudson/cli/CommandDuringBuild.java
@@ -36,7 +36,9 @@ import java.io.IOException;
* Base class for those commands that are valid only during a build.
*
* @author Kohsuke Kawaguchi
+ * @deprecated Limited to Remoting-based protocol.
*/
+@Deprecated
public abstract class CommandDuringBuild extends CLICommand {
/**
* This method makes sense only when called from within the build kicked by Jenkins.
diff --git a/core/src/main/java/hudson/cli/ConsoleCommand.java b/core/src/main/java/hudson/cli/ConsoleCommand.java
index f1069b857ae5eeab999863b512e33d4e8f8b407e..379cd8132be6ae36d3ba54968a2a4a65dcfafbcb 100644
--- a/core/src/main/java/hudson/cli/ConsoleCommand.java
+++ b/core/src/main/java/hudson/cli/ConsoleCommand.java
@@ -75,6 +75,7 @@ public class ConsoleCommand extends CLICommand {
do {
logText = run.getLogText();
pos = logText.writeLogTo(pos, w);
+ // TODO should sleep as in Run.writeWholeLogTo
} while (!logText.isComplete());
} else {
try (InputStream logInputStream = run.getLogInputStream()) {
diff --git a/core/src/main/java/hudson/cli/HelpCommand.java b/core/src/main/java/hudson/cli/HelpCommand.java
index 295e6e0e8e966f9e724d4eda231dff37b1e0952f..60fcc0970be1f6faa8badffcdfa0b1dde132a42c 100644
--- a/core/src/main/java/hudson/cli/HelpCommand.java
+++ b/core/src/main/java/hudson/cli/HelpCommand.java
@@ -53,7 +53,7 @@ public class HelpCommand extends CLICommand {
protected int run() throws Exception {
if (!Jenkins.getActiveInstance().hasPermission(Jenkins.READ)) {
throw new AccessDeniedException("You must authenticate to access this Jenkins.\n"
- + "Use --username/--password/--password-file parameters or login command.");
+ + hudson.cli.client.Messages.CLI_Usage());
}
if (command != null)
diff --git a/core/src/main/java/hudson/cli/InstallPluginCommand.java b/core/src/main/java/hudson/cli/InstallPluginCommand.java
index c21a96628d5f55fd7ac38145c2cde2938a12d30e..e04032893625736e4544ece5ba787a09172b0e17 100644
--- a/core/src/main/java/hudson/cli/InstallPluginCommand.java
+++ b/core/src/main/java/hudson/cli/InstallPluginCommand.java
@@ -42,6 +42,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
+import org.apache.commons.io.FileUtils;
/**
* Installs a plugin either from a file, an URL, or from update center.
@@ -55,14 +56,15 @@ public class InstallPluginCommand extends CLICommand {
return Messages.InstallPluginCommand_ShortDescription();
}
- @Argument(metaVar="SOURCE",required=true,usage="If this points to a local file, that file will be installed. " +
- "If this is an URL, Jenkins downloads the URL and installs that as a plugin." +
- "Otherwise the name is assumed to be the short name of the plugin in the existing update center (like \"findbugs\")," +
+ @Argument(metaVar="SOURCE",required=true,usage="If this points to a local file (‘-remoting’ mode only), that file will be installed. " +
+ "If this is an URL, Jenkins downloads the URL and installs that as a plugin. " +
+ "If it is the string ‘=’, the file will be read from standard input of the command, and ‘-name’ must be specified. " +
+ "Otherwise the name is assumed to be the short name of the plugin in the existing update center (like ‘findbugs’), " +
"and the plugin will be installed from the update center.")
public List sources = new ArrayList();
@Option(name="-name",usage="If specified, the plugin will be installed as this short name (whereas normally the name is inferred from the source name automatically).")
- public String name;
+ public String name; // TODO better to parse out Short-Name from the manifest and deprecate this option
@Option(name="-restart",usage="Restart Jenkins upon successful installation.")
public boolean restart;
@@ -80,6 +82,19 @@ public class InstallPluginCommand extends CLICommand {
}
for (String source : sources) {
+ if (source.equals("=")) {
+ if (name == null) {
+ throw new IllegalArgumentException("-name required when using -source -");
+ }
+ stdout.println(Messages.InstallPluginCommand_InstallingPluginFromStdin());
+ File f = getTargetFile(name);
+ FileUtils.copyInputStreamToFile(stdin, f);
+ if (dynamicLoad) {
+ pm.dynamicLoad(f);
+ }
+ continue;
+ }
+
// is this a file?
if (channel!=null) {
FilePath f = new FilePath(channel, source);
diff --git a/core/src/main/java/hudson/cli/InstallToolCommand.java b/core/src/main/java/hudson/cli/InstallToolCommand.java
index c387241f5a9066aadba2166f9faa4c23f6f8efd5..ef8db698f99df0f20da2a91c5092fc8804870d93 100644
--- a/core/src/main/java/hudson/cli/InstallToolCommand.java
+++ b/core/src/main/java/hudson/cli/InstallToolCommand.java
@@ -48,7 +48,9 @@ import org.kohsuke.args4j.Argument;
* Performs automatic tool installation on demand.
*
* @author Kohsuke Kawaguchi
+ * @deprecated Limited to Remoting-based protocol.
*/
+@Deprecated
@Extension
public class InstallToolCommand extends CLICommand {
@Argument(index=0,metaVar="KIND",usage="The type of the tool to install, such as 'Ant'")
diff --git a/core/src/main/java/hudson/cli/LoginCommand.java b/core/src/main/java/hudson/cli/LoginCommand.java
index c27b09d5100eb03863030631d03a80b097b7ce10..8920ad41114313104b356c8e5d9687558befd9c1 100644
--- a/core/src/main/java/hudson/cli/LoginCommand.java
+++ b/core/src/main/java/hudson/cli/LoginCommand.java
@@ -1,6 +1,7 @@
package hudson.cli;
import hudson.Extension;
+import java.io.PrintStream;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.kohsuke.args4j.CmdLineException;
@@ -10,14 +11,22 @@ import org.kohsuke.args4j.CmdLineException;
*
* @author Kohsuke Kawaguchi
* @since 1.351
+ * @deprecated Assumes Remoting, and vulnerable to JENKINS-12543.
*/
@Extension
+@Deprecated
public class LoginCommand extends CLICommand {
@Override
public String getShortDescription() {
return Messages.LoginCommand_ShortDescription();
}
+ @Override
+ protected void printUsageSummary(PrintStream stderr) {
+ super.printUsageSummary(stderr);
+ stderr.println(Messages.LoginCommand_FullDescription());
+ }
+
/**
* If we use the stored authentication for the login command, login becomes no-op, which is clearly not what
* the user has intended.
diff --git a/core/src/main/java/hudson/cli/LogoutCommand.java b/core/src/main/java/hudson/cli/LogoutCommand.java
index 80b2acce9b6302c569161b0f24aee72d9e2e532f..822c99d84ef1747ec60365513d4e8cce9587126d 100644
--- a/core/src/main/java/hudson/cli/LogoutCommand.java
+++ b/core/src/main/java/hudson/cli/LogoutCommand.java
@@ -1,13 +1,16 @@
package hudson.cli;
import hudson.Extension;
+import java.io.PrintStream;
/**
* Deletes the credential stored with the login command.
*
* @author Kohsuke Kawaguchi
* @since 1.351
+ * @deprecated See {@link LoginCommand}.
*/
+@Deprecated
@Extension
public class LogoutCommand extends CLICommand {
@Override
@@ -15,6 +18,12 @@ public class LogoutCommand extends CLICommand {
return Messages.LogoutCommand_ShortDescription();
}
+ @Override
+ protected void printUsageSummary(PrintStream stderr) {
+ super.printUsageSummary(stderr);
+ stderr.println(Messages.LogoutCommand_FullDescription());
+ }
+
@Override
protected int run() throws Exception {
ClientAuthenticationCache store = new ClientAuthenticationCache(checkChannel());
diff --git a/core/src/main/java/hudson/cli/SetBuildParameterCommand.java b/core/src/main/java/hudson/cli/SetBuildParameterCommand.java
index a54f0173e6325566f3e19fb193d26c6a8cfc9af5..2dfd0d6e8319d449a07db4344f93e5f67ea68af8 100644
--- a/core/src/main/java/hudson/cli/SetBuildParameterCommand.java
+++ b/core/src/main/java/hudson/cli/SetBuildParameterCommand.java
@@ -15,7 +15,9 @@ import java.util.Collections;
*
* @author Kohsuke Kawaguchi
* @since 1.514
+ * @deprecated Limited to Remoting-based protocol.
*/
+@Deprecated
@Extension
public class SetBuildParameterCommand extends CommandDuringBuild {
@Argument(index=0, metaVar="NAME", required=true, usage="Name of the build variable")
diff --git a/core/src/main/java/hudson/cli/SetBuildResultCommand.java b/core/src/main/java/hudson/cli/SetBuildResultCommand.java
index 82756472c7b3ac09f7b6276e472512d4ad8df314..ccdcc04c116e3b4eea0e7e6c794be5948bd453d8 100644
--- a/core/src/main/java/hudson/cli/SetBuildResultCommand.java
+++ b/core/src/main/java/hudson/cli/SetBuildResultCommand.java
@@ -33,7 +33,9 @@ import org.kohsuke.args4j.Argument;
* Sets the result of the current build. Works only if invoked from within a build.
*
* @author Kohsuke Kawaguchi
+ * @deprecated Limited to Remoting-based protocol.
*/
+@Deprecated
@Extension
public class SetBuildResultCommand extends CommandDuringBuild {
@Argument(metaVar="RESULT",required=true)
diff --git a/core/src/main/java/hudson/cli/util/ScriptLoader.java b/core/src/main/java/hudson/cli/util/ScriptLoader.java
index 8484c1e95926e09ecb20876dda6d5a3bdf0962c9..2bdbf0253da1397a63fa1b9db17dd2fa58873908 100644
--- a/core/src/main/java/hudson/cli/util/ScriptLoader.java
+++ b/core/src/main/java/hudson/cli/util/ScriptLoader.java
@@ -15,7 +15,9 @@ import java.net.URL;
* Reads a file (either a path or URL) over a channel.
*
* @author vjuranek
+ * @deprecated Specific to Remoting-based protocol.
*/
+@Deprecated
public class ScriptLoader extends MasterToSlaveCallable {
private final String script;
diff --git a/core/src/main/java/hudson/model/FileParameterDefinition.java b/core/src/main/java/hudson/model/FileParameterDefinition.java
index c57d2bafc664f054b2a871240e864ada2f6eb20a..e7160796cdf7faf757a9247b24306b74b2725099 100644
--- a/core/src/main/java/hudson/model/FileParameterDefinition.java
+++ b/core/src/main/java/hudson/model/FileParameterDefinition.java
@@ -23,18 +23,18 @@
*/
package hudson.model;
-import net.sf.json.JSONObject;
-import org.jenkinsci.Symbol;
-import org.kohsuke.stapler.DataBoundConstructor;
-import org.kohsuke.stapler.StaplerRequest;
import hudson.Extension;
import hudson.FilePath;
import hudson.cli.CLICommand;
-import org.apache.commons.fileupload.FileItem;
-
-import java.io.IOException;
import java.io.File;
+import java.io.IOException;
import javax.servlet.ServletException;
+import net.sf.json.JSONObject;
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.io.FileUtils;
+import org.jenkinsci.Symbol;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.StaplerRequest;
/**
* {@link ParameterDefinition} for doing file upload.
@@ -99,14 +99,22 @@ public class FileParameterDefinition extends ParameterDefinition {
return possiblyPathName;
}
+ @SuppressWarnings("deprecation")
@Override
public ParameterValue createValue(CLICommand command, String value) throws IOException, InterruptedException {
// capture the file to the server
- FilePath src = new FilePath(command.checkChannel(),value);
File local = File.createTempFile("jenkins","parameter");
- src.copyTo(new FilePath(local));
+ String name;
+ if (value.isEmpty()) {
+ FileUtils.copyInputStreamToFile(command.stdin, local);
+ name = "stdin";
+ } else {
+ FilePath src = new FilePath(command.checkChannel(), value);
+ src.copyTo(new FilePath(local));
+ name = src.getName();
+ }
- FileParameterValue p = new FileParameterValue(getName(), local, src.getName());
+ FileParameterValue p = new FileParameterValue(getName(), local, name);
p.setDescription(getDescription());
p.setLocation(getName());
return p;
diff --git a/core/src/main/java/hudson/model/FullDuplexHttpChannel.java b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java
index c0fdafb56f2b71028544dfa7d591d9c2237c3c9b..70e8c269f228d71e4fe189ce3ae52b4e3a79dca3 100644
--- a/core/src/main/java/hudson/model/FullDuplexHttpChannel.java
+++ b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java
@@ -23,142 +23,67 @@
*/
package hudson.model;
-import jenkins.util.SystemProperties;
import hudson.remoting.Channel;
import hudson.remoting.PingThread;
import hudson.remoting.Channel.Mode;
-import hudson.util.ChunkedOutputStream;
-import hudson.util.ChunkedInputStream;
-import org.kohsuke.accmod.Restricted;
-import org.kohsuke.accmod.restrictions.NoExternalUse;
-import org.kohsuke.stapler.StaplerRequest;
-import org.kohsuke.stapler.StaplerResponse;
-
-import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
-import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
+import jenkins.util.FullDuplexHttpService;
/**
* Builds a {@link Channel} on top of two HTTP streams (one used for each direction.)
*
* @author Kohsuke Kawaguchi
*/
-abstract public class FullDuplexHttpChannel {
+abstract public class FullDuplexHttpChannel extends FullDuplexHttpService {
private Channel channel;
-
- private InputStream upload;
-
- private final UUID uuid;
private final boolean restricted;
- private boolean completed;
-
public FullDuplexHttpChannel(UUID uuid, boolean restricted) throws IOException {
- this.uuid = uuid;
+ super(uuid);
this.restricted = restricted;
}
- /**
- * This is where we send the data to the client.
- *
- *
- * If this connection is lost, we'll abort the channel.
- */
- public synchronized void download(StaplerRequest req, StaplerResponse rsp) throws InterruptedException, IOException {
- rsp.setStatus(HttpServletResponse.SC_OK);
-
- // server->client channel.
- // this is created first, and this controls the lifespan of the channel
- rsp.addHeader("Transfer-Encoding", "chunked");
- OutputStream out = rsp.getOutputStream();
- if (DIY_CHUNKING) out = new ChunkedOutputStream(out);
-
- // send something out so that the client will see the HTTP headers
- out.write("Starting HTTP duplex channel".getBytes());
- out.flush();
-
- {// wait until we have the other channel
- long end = System.currentTimeMillis() + CONNECTION_TIMEOUT;
- while (upload == null && System.currentTimeMillis()
+ * If this connection is lost, we'll abort the channel.
+ */
+ public synchronized void download(StaplerRequest req, StaplerResponse rsp) throws InterruptedException, IOException {
+ rsp.setStatus(HttpServletResponse.SC_OK);
+
+ // server->client channel.
+ // this is created first, and this controls the lifespan of the channel
+ rsp.addHeader("Transfer-Encoding", "chunked");
+ OutputStream out = rsp.getOutputStream();
+ if (DIY_CHUNKING) {
+ out = new ChunkedOutputStream(out);
+ }
+
+ // send something out so that the client will see the HTTP headers
+ out.write(0);
+ out.flush();
+
+ {// wait until we have the other channel
+ long end = System.currentTimeMillis() + CONNECTION_TIMEOUT;
+ while (upload == null && System.currentTimeMillis() < end) {
+ wait(1000);
+ }
+
+ if (upload == null) {
+ throw new IOException("HTTP full-duplex channel timeout: " + uuid);
+ }
+ }
+
+ try {
+ run(upload, out);
+ } finally {
+ // publish that we are done
+ completed = true;
+ notify();
+ }
+ }
+
+ protected abstract void run(InputStream upload, OutputStream download) throws IOException, InterruptedException;
+
+ /**
+ * This is where we receive inputs from the client.
+ */
+ public synchronized void upload(StaplerRequest req, StaplerResponse rsp) throws InterruptedException, IOException {
+ rsp.setStatus(HttpServletResponse.SC_OK);
+ InputStream in = req.getInputStream();
+ if (DIY_CHUNKING) {
+ in = new ChunkedInputStream(in);
+ }
+
+ // publish the upload channel
+ upload = in;
+ notify();
+
+ // wait until we are done
+ while (!completed) {
+ wait();
+ }
+ }
+
+ /**
+ * HTTP response that allows a client to use this service.
+ */
+ public static abstract class Response extends HttpResponses.HttpResponseException {
+
+ private final Map services;
+
+ /**
+ * @param services a cross-request cache of services, to correlate the
+ * upload and download connections
+ */
+ protected Response(Map services) {
+ this.services = services;
+ }
+
+ @Override
+ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
+ try {
+ // do not require any permission to establish a CLI connection
+ // the actual authentication for the connecting Channel is done by CLICommand
+
+ UUID uuid = UUID.fromString(req.getHeader("Session"));
+ rsp.setHeader("Hudson-Duplex", "true"); // set the header so that the client would know
+
+ if (req.getHeader("Side").equals("download")) {
+ FullDuplexHttpService service = createService(req, uuid);
+ services.put(uuid, service);
+ try {
+ service.download(req, rsp);
+ } finally {
+ services.remove(uuid);
+ }
+ } else {
+ services.get(uuid).upload(req, rsp);
+ }
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ }
+ }
+
+ protected abstract FullDuplexHttpService createService(StaplerRequest req, UUID uuid) throws IOException, InterruptedException;
+
+ }
+
+}
diff --git a/core/src/main/resources/hudson/cli/Messages.properties b/core/src/main/resources/hudson/cli/Messages.properties
index 997193ad61b509d453c58ff7de0ad82971ad0cc1..8b9c933e6dc90092f473b1a0cfbf71842e02acba 100644
--- a/core/src/main/resources/hudson/cli/Messages.properties
+++ b/core/src/main/resources/hudson/cli/Messages.properties
@@ -1,6 +1,7 @@
InstallPluginCommand.DidYouMean={0} looks like a short plugin name. Did you mean \u2018{1}\u2019?
InstallPluginCommand.InstallingFromUpdateCenter=Installing {0} from update center
InstallPluginCommand.InstallingPluginFromLocalFile=Installing a plugin from local file: {0}
+InstallPluginCommand.InstallingPluginFromStdin=Installing a plugin from standard input
InstallPluginCommand.InstallingPluginFromUrl=Installing a plugin from {0}
InstallPluginCommand.NoUpdateCenterDefined=Note that no update center is defined in this Jenkins.
InstallPluginCommand.NoUpdateDataRetrieved=No update center data is retrieved yet from: {0}
@@ -33,7 +34,7 @@ HelpCommand.ShortDescription=\
InstallPluginCommand.ShortDescription=\
Installs a plugin either from a file, an URL, or from update center.
InstallToolCommand.ShortDescription=\
- Performs automatic tool installation, and print its location to stdout. Can be only called from inside a build.
+ Performs automatic tool installation, and print its location to stdout. Can be only called from inside a build. [deprecated]
ListChangesCommand.ShortDescription=\
Dumps the changelog for the specified build(s).
ListJobsCommand.ShortDescription=\
@@ -41,16 +42,27 @@ ListJobsCommand.ShortDescription=\
ListPluginsCommand.ShortDescription=\
Outputs a list of installed plugins.
LoginCommand.ShortDescription=\
- Saves the current credential to allow future commands to run without explicit credential information.
+ Saves the current credentials to allow future commands to run without explicit credential information. [deprecated]
+LoginCommand.FullDescription=\
+ Depending on the security realm, you will need to pass something like:\n\
+ --username USER [ --password PASS | --password-file FILE ]\n\
+ May not be supported in some security realms, such as single-sign-on.\n\
+ Pair with the logout command.\n\
+ The same options can be used on any other command, but unlike other generic CLI options,\n\
+ these come *after* the command name.\n\
+ Whether stored or not, this authentication mode will not let you refer to (e.g.) jobs as arguments\n\
+ if Jenkins denies anonymous users Overall/Read and (e.g.) Job/Read.\n\
+ *Deprecated* in favor of -auth.
LogoutCommand.ShortDescription=\
- Deletes the credential stored with the login command.
+ Deletes the credentials stored with the login command. [deprecated]
+LogoutCommand.FullDescription=*Deprecated* in favor of -auth.
MailCommand.ShortDescription=\
Reads stdin and sends that out as an e-mail.
SetBuildDescriptionCommand.ShortDescription=\
Sets the description of a build.
-SetBuildParameterCommand.ShortDescription=Update/set the build parameter of the current build in progress.
+SetBuildParameterCommand.ShortDescription=Update/set the build parameter of the current build in progress. [deprecated]
SetBuildResultCommand.ShortDescription=\
- Sets the result of the current build. Works only if invoked from within a build.
+ Sets the result of the current build. Works only if invoked from within a build. [deprecated]
RemoveJobFromViewCommand.ShortDescription=\
Removes jobs from view.
VersionCommand.ShortDescription=\
diff --git a/core/src/main/resources/jenkins/CLI/WarnWhenEnabled/message.jelly b/core/src/main/resources/jenkins/CLI/WarnWhenEnabled/message.jelly
new file mode 100644
index 0000000000000000000000000000000000000000..5f93d73ee710a380c262e9e00b4e2047347af8de
--- /dev/null
+++ b/core/src/main/resources/jenkins/CLI/WarnWhenEnabled/message.jelly
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/jenkins/CLI/WarnWhenEnabled/message.properties b/core/src/main/resources/jenkins/CLI/WarnWhenEnabled/message.properties
new file mode 100644
index 0000000000000000000000000000000000000000..2b6c3980d584a874806a5d931e1a6d9aa35b7332
--- /dev/null
+++ b/core/src/main/resources/jenkins/CLI/WarnWhenEnabled/message.properties
@@ -0,0 +1,26 @@
+# The MIT License
+#
+# Copyright 2017 CloudBees, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+blurb=\
+ Allowing Jenkins CLI to work in -remoting
mode is considered dangerous and usually unnecessary. \
+ You are advised to disable this mode. \
+ Please refer to the CLI documentation for details.
diff --git a/core/src/main/resources/jenkins/CLI/config.jelly b/core/src/main/resources/jenkins/CLI/config.jelly
new file mode 100644
index 0000000000000000000000000000000000000000..001bca27db7e562028d9d47f3eb31e9e6edc2fea
--- /dev/null
+++ b/core/src/main/resources/jenkins/CLI/config.jelly
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/jenkins/CLI/help-enabled.html b/core/src/main/resources/jenkins/CLI/help-enabled.html
new file mode 100644
index 0000000000000000000000000000000000000000..8301e1fdebc15aa5f1d5c625961c1215ee18a1b5
--- /dev/null
+++ b/core/src/main/resources/jenkins/CLI/help-enabled.html
@@ -0,0 +1,8 @@
+
+ Whether to enable the historical Jenkins CLI mode over remoting
+ (-remoting
option in the client).
+ While this may be necessary to support certain commands or command options,
+ it is considered intrinsically insecure.
+ (-http
mode is always available,
+ and -ssh
mode is available whenever the SSH service is enabled.)
+
diff --git a/pom.xml b/pom.xml
index 1703c8444e1074acd70d9fcd09a0445738a17210..f7ef5f54b81cbcb9ce4c4355edecad32dc5f1d06 100644
--- a/pom.xml
+++ b/pom.xml
@@ -108,12 +108,7 @@ THE SOFTWARE.
repo.jenkins-ci.org
http://repo.jenkins-ci.org/public/
-
- true
-
-
- false
-
+
@@ -121,12 +116,6 @@ THE SOFTWARE.
repo.jenkins-ci.org
http://repo.jenkins-ci.org/public/
-
- true
-
-
- false
-
@@ -202,11 +191,6 @@ THE SOFTWARE.
slf4j-api
${slf4jVersion}
-
- org.slf4j
- slf4j-nop
- ${slf4jVersion}
-
org.slf4j
slf4j-jdk14
@@ -392,6 +376,7 @@ THE SOFTWARE.
3600
true
+ false
diff --git a/test/pom.xml b/test/pom.xml
index 0106d58916ff02692986d42d863ecb9a8b538898..f87e530d72f9033976d0a2d5052dda5cd943c597 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -40,7 +40,6 @@ THE SOFTWARE.
2
false
- false
@@ -53,12 +52,6 @@ THE SOFTWARE.
jenkins-war
${project.version}
war-for-test
-
-
- org.jenkins-ci.modules
- sshd
-
-
${project.groupId}
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"]);
diff --git a/test/src/test/java/hudson/cli/BuildCommand2Test.java b/test/src/test/java/hudson/cli/BuildCommand2Test.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f5366e1bbfc6bcad7170bb31e8574ef66023393
--- /dev/null
+++ b/test/src/test/java/hudson/cli/BuildCommand2Test.java
@@ -0,0 +1,74 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.cli;
+
+import hudson.Launcher;
+import hudson.model.AbstractBuild;
+import hudson.model.BuildListener;
+import hudson.model.FileParameterDefinition;
+import hudson.model.FreeStyleBuild;
+import hudson.model.FreeStyleProject;
+import hudson.model.ParametersDefinitionProperty;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import org.junit.ClassRule;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.Rule;
+import org.jvnet.hudson.test.BuildWatcher;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.TestBuilder;
+
+public class BuildCommand2Test {
+
+ @ClassRule
+ public static BuildWatcher buildWatcher = new BuildWatcher();
+
+ @Rule
+ public JenkinsRule r = new JenkinsRule();
+
+ @Issue("JENKINS-41745")
+ @Test
+ public void fileParameter() throws Exception {
+ FreeStyleProject p = r.createFreeStyleProject("myjob");
+ p.addProperty(new ParametersDefinitionProperty(new FileParameterDefinition("file", null)));
+ p.getBuildersList().add(new TestBuilder() {
+ @Override
+ public boolean perform(AbstractBuild, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
+ listener.getLogger().println("Found in my workspace: " + build.getWorkspace().child("file").readToString());
+ return true;
+ }
+ });
+ assertThat(new CLICommandInvoker(r, "build").
+ withStdin(new ByteArrayInputStream("uploaded content here".getBytes())).
+ invokeWithArgs("-f", "-p", "file=", "myjob"),
+ CLICommandInvoker.Matcher.succeeded());
+ FreeStyleBuild b = p.getBuildByNumber(1);
+ assertNotNull(b);
+ r.assertLogContains("uploaded content here", b);
+ }
+
+}
diff --git a/test/src/test/java/hudson/cli/CLIActionTest.java b/test/src/test/java/hudson/cli/CLIActionTest.java
index 8a808673a48311bc4233df0ca67c2d925ec70684..62ab20ce38581db792df980c955d104eb0c14521 100644
--- a/test/src/test/java/hudson/cli/CLIActionTest.java
+++ b/test/src/test/java/hudson/cli/CLIActionTest.java
@@ -4,29 +4,64 @@ import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
+import com.google.common.collect.Lists;
import hudson.Functions;
+import hudson.Launcher;
+import hudson.Proc;
+import hudson.model.Item;
+import hudson.model.User;
import hudson.remoting.Channel;
import hudson.remoting.ChannelBuilder;
+import hudson.util.ProcessTree;
+import hudson.util.StreamTaskListener;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import jenkins.model.Jenkins;
+import jenkins.security.ApiTokenProperty;
+import jenkins.util.Timer;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.output.TeeOutputStream;
import org.codehaus.groovy.runtime.Security218;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.LoggerRule;
+import org.jvnet.hudson.test.MockAuthorizationStrategy;
+import org.jvnet.hudson.test.TestExtension;
import org.jvnet.hudson.test.recipes.PresetData;
import org.jvnet.hudson.test.recipes.PresetData.DataSet;
public class CLIActionTest {
@Rule
public JenkinsRule j = new JenkinsRule();
+ { // authentication() can take a while on a loaded machine
+ j.timeout = System.getProperty("maven.surefire.debug") == null ? 300 : 0;
+ }
+
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ @Rule
+ public LoggerRule logging = new LoggerRule();
private ExecutorService pool;
@@ -37,6 +72,7 @@ public class CLIActionTest {
public void testDuplexHttp() throws Exception {
pool = Executors.newCachedThreadPool();
try {
+ @SuppressWarnings("deprecation") // to verify compatibility of original constructor
FullDuplexHttpStream con = new FullDuplexHttpStream(new URL(j.getURL(), "cli"), null);
Channel ch = new ChannelBuilder("test connection", pool).build(con.getInputStream(), con.getOutputStream());
ch.close();
@@ -49,7 +85,7 @@ public class CLIActionTest {
public void security218() throws Exception {
pool = Executors.newCachedThreadPool();
try {
- FullDuplexHttpStream con = new FullDuplexHttpStream(new URL(j.getURL(), "cli"), null);
+ FullDuplexHttpStream con = new FullDuplexHttpStream(j.getURL(), "cli", null);
Channel ch = new ChannelBuilder("test connection", pool).build(con.getInputStream(), con.getOutputStream());
ch.call(new Security218());
fail("Expected the call to be rejected");
@@ -61,7 +97,7 @@ public class CLIActionTest {
}
- @SuppressWarnings({"unchecked", "rawtypes"}) // intentionally passing an unreifiable argument here
+ @SuppressWarnings({"unchecked", "rawtypes", "deprecation"}) // intentionally passing an unreifiable argument here; Remoting-based constructor intentional
@Test
public void security218_take2() throws Exception {
pool = Executors.newCachedThreadPool();
@@ -102,4 +138,165 @@ public class CLIActionTest {
wc.goTo("cli");
}
+ @Issue({"JENKINS-12543", "JENKINS-41745"})
+ @Test
+ public void authentication() throws Exception {
+ logging.record(PlainCLIProtocol.class, Level.FINE);
+ File jar = tmp.newFile("jenkins-cli.jar");
+ FileUtils.copyURLToFile(j.jenkins.getJnlpJars("jenkins-cli.jar").getURL(), jar);
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to(ADMIN));
+ j.createFreeStyleProject("p");
+ // CLICommand with @Argument:
+ assertExitCode(3, false, jar, "-remoting", "get-job", "p"); // IllegalArgumentException from GenericItemOptionHandler
+ assertExitCode(3, false, jar, "get-job", "p"); // ditto under new protocol
+ assertExitCode(3, false, jar, "-remoting", "get-job", "--username", ADMIN, "--password", ADMIN, "p"); // JENKINS-12543: too late
+ assertExitCode(3, false, jar, "get-job", "--username", ADMIN, "--password", ADMIN, "p"); // same
+ assertExitCode(0, false, jar, "-remoting", "login", "--username", ADMIN, "--password", ADMIN);
+ try {
+ assertExitCode(3, false, jar, "-remoting", "get-job", "p"); // ClientAuthenticationCache also used too late
+ } finally {
+ assertExitCode(0, false, jar, "-remoting", "logout");
+ }
+ assertExitCode(3, true, jar, "-remoting", "get-job", "p"); // does not work with API tokens
+ assertExitCode(0, true, jar, "get-job", "p"); // but does under new protocol
+ // @CLIMethod:
+ assertExitCode(6, false, jar, "-remoting", "disable-job", "p"); // AccessDeniedException from CLIRegisterer?
+ assertExitCode(6, false, jar, "disable-job", "p");
+ assertExitCode(0, false, jar, "-remoting", "disable-job", "--username", ADMIN, "--password", ADMIN, "p"); // works from CliAuthenticator
+ assertExitCode(0, false, jar, "disable-job", "--username", ADMIN, "--password", ADMIN, "p");
+ assertExitCode(0, false, jar, "-remoting", "login", "--username", ADMIN, "--password", ADMIN);
+ try {
+ assertExitCode(0, false, jar, "-remoting", "disable-job", "p"); // or from ClientAuthenticationCache
+ } finally {
+ assertExitCode(0, false, jar, "-remoting", "logout");
+ }
+ assertExitCode(6, true, jar, "-remoting", "disable-job", "p");
+ assertExitCode(0, true, jar, "disable-job", "p");
+ // If we have anonymous read access, then the situation is simpler.
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to(ADMIN).grant(Jenkins.READ, Item.READ).everywhere().toEveryone());
+ assertExitCode(6, false, jar, "-remoting", "get-job", "p"); // AccessDeniedException from AbstractItem.writeConfigDotXml
+ assertExitCode(6, false, jar, "get-job", "p");
+ assertExitCode(0, false, jar, "-remoting", "get-job", "--username", ADMIN, "--password", ADMIN, "p");
+ assertExitCode(0, false, jar, "get-job", "--username", ADMIN, "--password", ADMIN, "p");
+ assertExitCode(0, false, jar, "-remoting", "login", "--username", ADMIN, "--password", ADMIN);
+ try {
+ assertExitCode(0, false, jar, "-remoting", "get-job", "p");
+ } finally {
+ assertExitCode(0, false, jar, "-remoting", "logout");
+ }
+ assertExitCode(6, true, jar, "-remoting", "get-job", "p"); // does not work with API tokens
+ assertExitCode(0, true, jar, "get-job", "p"); // but does under new protocol
+ assertExitCode(6, false, jar, "-remoting", "disable-job", "p"); // AccessDeniedException from AbstractProject.doDisable
+ assertExitCode(6, false, jar, "disable-job", "p");
+ assertExitCode(0, false, jar, "-remoting", "disable-job", "--username", ADMIN, "--password", ADMIN, "p");
+ assertExitCode(0, false, jar, "disable-job", "--username", ADMIN, "--password", ADMIN, "p");
+ assertExitCode(0, false, jar, "-remoting", "login", "--username", ADMIN, "--password", ADMIN);
+ try {
+ assertExitCode(0, false, jar, "-remoting", "disable-job", "p");
+ } finally {
+ assertExitCode(0, false, jar, "-remoting", "logout");
+ }
+ assertExitCode(6, true, jar, "-remoting", "disable-job", "p");
+ assertExitCode(0, true, jar, "disable-job", "p");
+ // Show that API tokens do work in Remoting-over-HTTP mode (just not over the JNLP port):
+ j.jenkins.setSlaveAgentPort(-1);
+ assertExitCode(0, true, jar, "-remoting", "get-job", "p");
+ assertExitCode(0, true, jar, "-remoting", "disable-job", "p");
+ }
+
+ private static final String ADMIN = "admin@mycorp.com";
+
+ private void assertExitCode(int code, boolean useApiToken, File jar, String... args) throws IOException, InterruptedException {
+ List commands = Lists.newArrayList("java", "-jar", jar.getAbsolutePath(), "-s", j.getURL().toString(), /* not covering SSH keys in this test */ "-noKeyAuth");
+ if (useApiToken) {
+ commands.add("-auth");
+ commands.add(ADMIN + ":" + User.get(ADMIN).getProperty(ApiTokenProperty.class).getApiToken());
+ }
+ commands.addAll(Arrays.asList(args));
+ final Launcher.LocalLauncher launcher = new Launcher.LocalLauncher(StreamTaskListener.fromStderr());
+ final Proc proc = launcher.launch().cmds(commands).stdout(System.out).stderr(System.err).start();
+ if (!Functions.isWindows()) {
+ // Try to get a thread dump of the client if it hangs.
+ Timer.get().schedule(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (proc.isAlive()) {
+ Field procF = Proc.LocalProc.class.getDeclaredField("proc");
+ procF.setAccessible(true);
+ ProcessTree.OSProcess osp = ProcessTree.get().get((Process) procF.get(proc));
+ if (osp != null) {
+ launcher.launch().cmds("kill", "-QUIT", Integer.toString(osp.getPid())).stdout(System.out).stderr(System.err).join();
+ }
+ }
+ } catch (Exception x) {
+ throw new AssertionError(x);
+ }
+ }
+ }, 1, TimeUnit.MINUTES);
+ }
+ assertEquals(code, proc.join());
+ }
+
+ @Issue("JENKINS-41745")
+ @Test
+ public void encodingAndLocale() throws Exception {
+ File jar = tmp.newFile("jenkins-cli.jar");
+ FileUtils.copyURLToFile(j.jenkins.getJnlpJars("jenkins-cli.jar").getURL(), jar);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ assertEquals(0, new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(
+ "java", "-Dfile.encoding=ISO-8859-2", "-Duser.language=cs", "-Duser.country=CZ", "-jar", jar.getAbsolutePath(),
+ "-s", j.getURL().toString()./* just checking */replaceFirst("/$", ""), "-noKeyAuth", "test-diagnostic").
+ stdout(baos).stderr(System.err).join());
+ assertEquals("encoding=ISO-8859-2 locale=cs_CZ", baos.toString().trim());
+ // TODO test that stdout/stderr are in expected encoding (not true of -remoting mode!)
+ // -ssh mode does not pass client locale or encoding
+ }
+
+ @Issue("JENKINS-41745")
+ @Test
+ public void interleavedStdio() throws Exception {
+ logging.record(PlainCLIProtocol.class, Level.FINE);
+ File jar = tmp.newFile("jenkins-cli.jar");
+ FileUtils.copyURLToFile(j.jenkins.getJnlpJars("jenkins-cli.jar").getURL(), jar);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PipedInputStream pis = new PipedInputStream();
+ PipedOutputStream pos = new PipedOutputStream(pis);
+ PrintWriter pw = new PrintWriter(new TeeOutputStream(pos, System.err), true);
+ Proc proc = new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(
+ "java", "-jar", jar.getAbsolutePath(), "-s", j.getURL().toString(), "-noKeyAuth", "groovysh").
+ stdout(new TeeOutputStream(baos, System.out)).stderr(System.err).stdin(pis).start();
+ while (!baos.toString().contains("000")) { // cannot just search for, say, "groovy:000> " since there are ANSI escapes there (cf. StringEscapeUtils.escapeJava)
+ Thread.sleep(100);
+ }
+ pw.println("11 * 11");
+ while (!baos.toString().contains("121")) { // ditto not "===> 121"
+ Thread.sleep(100);
+ }
+ Thread.sleep(31_000); // aggravate org.eclipse.jetty.io.IdleTimeout (cf. AbstractConnector._idleTimeout)
+ pw.println("11 * 11 * 11");
+ while (!baos.toString().contains("1331")) {
+ Thread.sleep(100);
+ }
+ pw.println(":q");
+ assertEquals(0, proc.join());
+ }
+
+ @TestExtension("encodingAndLocale")
+ public static class TestDiagnosticCommand extends CLICommand {
+
+ @Override
+ public String getShortDescription() {
+ return "Print information about the command environment.";
+ }
+
+ @Override
+ protected int run() throws Exception {
+ stdout.println("encoding=" + getClientCharset() + " locale=" + locale);
+ return 0;
+ }
+
+ }
+
}
diff --git a/test/src/test/java/hudson/cli/CLITest.java b/test/src/test/java/hudson/cli/CLITest.java
new file mode 100644
index 0000000000000000000000000000000000000000..19abf44b8fd5e13da7db97f2204b36be5ab9f626
--- /dev/null
+++ b/test/src/test/java/hudson/cli/CLITest.java
@@ -0,0 +1,152 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.cli;
+
+import com.google.common.collect.Lists;
+import hudson.Functions;
+import hudson.Launcher;
+import hudson.Proc;
+import hudson.model.FreeStyleProject;
+import hudson.model.User;
+import hudson.util.StreamTaskListener;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import jenkins.model.Jenkins;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.output.TeeOutputStream;
+import static org.hamcrest.Matchers.*;
+import org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl;
+import org.jenkinsci.main.modules.sshd.SSHD;
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.jvnet.hudson.test.BuildWatcher;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.MockAuthorizationStrategy;
+import org.jvnet.hudson.test.SleepBuilder;
+
+public class CLITest {
+
+ @ClassRule
+ public static BuildWatcher buildWatcher = new BuildWatcher();
+
+ @Rule
+ public JenkinsRule r = new JenkinsRule();
+
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ /** Sets up a fake {@code user.home} so that tests {@code -ssh} mode does not get confused by the developer’s real {@code ~/.ssh/known_hosts}. */
+ private File tempHome() throws IOException {
+ File home = tmp.newFolder();
+ // Seems it gets created automatically but with inappropriate permissions:
+ File known_hosts = new File(new File(home, ".ssh"), "known_hosts");
+ assumeTrue(known_hosts.getParentFile().mkdir());
+ assumeTrue(known_hosts.createNewFile());
+ assumeTrue(known_hosts.setWritable(false, false));
+ assumeTrue(known_hosts.setWritable(true, true));
+ try {
+ Files.getOwner(known_hosts.toPath());
+ } catch (IOException x) {
+ assumeNoException("Sometimes on Windows KnownHostsServerKeyVerifier.acceptIncompleteHostKeys says WARNING: Failed (FileSystemException) to reload server keys from …\\\\.ssh\\\\known_hosts: … Incorrect function.", x);
+ }
+ /* TODO impossible to do this until the bundled sshd module uses a sufficiently new version of sshd-core:
+ assumeThat("or on Windows DefaultKnownHostsServerKeyVerifier.reloadKnownHosts says invalid file permissions: Owner violation (Administrators)",
+ ModifiableFileWatcher.validateStrictConfigFilePermissions(known_hosts.toPath()), nullValue());
+ */
+ assumeFalse(Functions.isWindows()); // TODO can remove when above check is restored
+ return home;
+ }
+
+ @Issue("JENKINS-41745")
+ @Test
+ public void strictHostKey() throws Exception {
+ File home = tempHome();
+ r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
+ r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("admin"));
+ SSHD.get().setPort(0);
+ File jar = tmp.newFile("jenkins-cli.jar");
+ FileUtils.copyURLToFile(r.jenkins.getJnlpJars("jenkins-cli.jar").getURL(), jar);
+ File privkey = tmp.newFile("id_rsa");
+ FileUtils.copyURLToFile(CLITest.class.getResource("id_rsa"), privkey);
+ User.get("admin").addProperty(new UserPropertyImpl(IOUtils.toString(CLITest.class.getResource("id_rsa.pub"))));
+ assertNotEquals(0, new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(
+ "java", "-Duser.home=" + home, "-jar", jar.getAbsolutePath(), "-s", r.getURL().toString(), "-ssh", "-user", "admin", "-i", privkey.getAbsolutePath(), "-strictHostKey", "who-am-i"
+ ).stdout(System.out).stderr(System.err).join());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ assertEquals(0, new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(
+ "java", "-Duser.home=" + home, "-jar", jar.getAbsolutePath(), "-s", r.getURL().toString(), "-ssh", "-user", "admin", "-i", privkey.getAbsolutePath(), "-logger", "FINEST", "who-am-i"
+ ).stdout(baos).stderr(System.err).join());
+ assertThat(baos.toString(), containsString("Authenticated as: admin"));
+ baos = new ByteArrayOutputStream();
+ assertEquals(0, new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(
+ "java", "-Duser.home=" + home, "-jar", jar.getAbsolutePath(), "-s", r.getURL().toString()./* just checking */replaceFirst("/$", ""), "-ssh", "-user", "admin", "-i", privkey.getAbsolutePath(), "-strictHostKey", "who-am-i"
+ ).stdout(baos).stderr(System.err).join());
+ assertThat(baos.toString(), containsString("Authenticated as: admin"));
+ }
+
+ @Issue("JENKINS-41745")
+ @Test
+ public void interrupt() throws Exception {
+ File home = tempHome();
+ r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
+ r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("admin"));
+ SSHD.get().setPort(0);
+ File jar = tmp.newFile("jenkins-cli.jar");
+ FileUtils.copyURLToFile(r.jenkins.getJnlpJars("jenkins-cli.jar").getURL(), jar);
+ File privkey = tmp.newFile("id_rsa");
+ FileUtils.copyURLToFile(CLITest.class.getResource("id_rsa"), privkey);
+ User.get("admin").addProperty(new UserPropertyImpl(IOUtils.toString(CLITest.class.getResource("id_rsa.pub"))));
+ FreeStyleProject p = r.createFreeStyleProject("p");
+ p.getBuildersList().add(new SleepBuilder(TimeUnit.MINUTES.toMillis(5)));
+ doInterrupt(jar, home, p, "-remoting", "-i", privkey.getAbsolutePath());
+ doInterrupt(jar, home, p, "-ssh", "-user", "admin", "-i", privkey.getAbsolutePath());
+ doInterrupt(jar, home, p, "-http", "-auth", "admin:admin");
+ }
+ private void doInterrupt(File jar, File home, FreeStyleProject p, String... modeArgs) throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ List args = Lists.newArrayList("java", "-Duser.home=" + home, "-jar", jar.getAbsolutePath(), "-s", r.getURL().toString());
+ args.addAll(Arrays.asList(modeArgs));
+ args.addAll(Arrays.asList("build", "-s", "-v", "p"));
+ Proc proc = new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(args).stdout(new TeeOutputStream(baos, System.out)).stderr(System.err).start();
+ while (!baos.toString().contains("Sleeping ")) {
+ Thread.sleep(100);
+ }
+ System.err.println("Killing client");
+ proc.kill();
+ r.waitForCompletion(p.getLastBuild());
+ }
+
+}
diff --git a/test/src/test/java/hudson/cli/GetJobCommandTest.java b/test/src/test/java/hudson/cli/GetJobCommandTest.java
index 8a52cfed0ccb2f41ae4d6d14eeb7ba59b575dd27..878cb022484c9676f5410262663bb45060361806 100644
--- a/test/src/test/java/hudson/cli/GetJobCommandTest.java
+++ b/test/src/test/java/hudson/cli/GetJobCommandTest.java
@@ -47,6 +47,7 @@ public class GetJobCommandTest {
FreeStyleProject p = d.createProject(FreeStyleProject.class, "p");
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream outS = new PrintStream(out);
+ // TODO switch to CLICommandInvoker
int result = new GetJobCommand().main(Collections.singletonList("d/p"), Locale.ENGLISH, new NullInputStream(0), outS, outS);
outS.flush();
String output = out.toString();
diff --git a/test/src/test/java/hudson/cli/InstallPluginCommandTest.java b/test/src/test/java/hudson/cli/InstallPluginCommandTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c84031bc82dff43007bc6c03c4ea7f69091cb624
--- /dev/null
+++ b/test/src/test/java/hudson/cli/InstallPluginCommandTest.java
@@ -0,0 +1,49 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.cli;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.Rule;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+
+public class InstallPluginCommandTest {
+
+ @Rule
+ public JenkinsRule r = new JenkinsRule();
+
+ @Issue("JENKINS-41745")
+ @Test
+ public void fromStdin() throws Exception {
+ assertNull(r.jenkins.getPluginManager().getPlugin("token-macro"));
+ assertThat(new CLICommandInvoker(r, "install-plugin").
+ withStdin(InstallPluginCommandTest.class.getResourceAsStream("/plugins/token-macro.hpi")).
+ invokeWithArgs("-name", "token-macro", "-deploy", "="),
+ CLICommandInvoker.Matcher.succeeded());
+ assertNotNull(r.jenkins.getPluginManager().getPlugin("token-macro"));
+ }
+
+}
diff --git a/test/src/test/java/hudson/model/ComputerSetTest.java b/test/src/test/java/hudson/model/ComputerSetTest.java
index 56878bf4cda64f08e3b3bdfeedffb893aadb3734..38418e9cc00cc175a3bb909f3ca3e0ab7bb95e84 100644
--- a/test/src/test/java/hudson/model/ComputerSetTest.java
+++ b/test/src/test/java/hudson/model/ComputerSetTest.java
@@ -23,18 +23,13 @@
*/
package hudson.model;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.empty;
-import static org.hamcrest.Matchers.is;
-import static org.junit.Assert.assertTrue;
-
-import hudson.cli.CLI;
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import hudson.cli.CLICommandInvoker;
import hudson.slaves.DumbSlave;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
-import com.gargoylesoftware.htmlunit.html.HtmlForm;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
@@ -69,14 +64,12 @@ public class ComputerSetTest {
public void nodeOfflineCli() throws Exception {
DumbSlave s = j.createSlave();
- try (CLI cli = new CLI(j.getURL())) {
- assertTrue(cli.execute("wait-node-offline","xxx")!=0);
- assertTrue(cli.execute("wait-node-online",s.getNodeName())==0);
+ assertThat(new CLICommandInvoker(j, "wait-node-offline").invokeWithArgs("xxx"), CLICommandInvoker.Matcher.failedWith(/* IllegalArgumentException from NodeOptionHandler */ 3));
+ assertThat(new CLICommandInvoker(j, "wait-node-online").invokeWithArgs(s.getNodeName()), CLICommandInvoker.Matcher.succeededSilently());
- s.toComputer().disconnect().get();
+ s.toComputer().disconnect(null).get();
- assertTrue(cli.execute("wait-node-offline",s.getNodeName())==0);
- }
+ assertThat(new CLICommandInvoker(j, "wait-node-offline").invokeWithArgs(s.getNodeName()), CLICommandInvoker.Matcher.succeededSilently());
}
@Test
diff --git a/test/src/test/java/hudson/model/listeners/ItemListenerTest.java b/test/src/test/java/hudson/model/listeners/ItemListenerTest.java
index bf19e6ea0d513c78cdf55a9301f05cb01d3a41c0..85d3160154a9fcc2a9c4df416b96181506f8c08b 100644
--- a/test/src/test/java/hudson/model/listeners/ItemListenerTest.java
+++ b/test/src/test/java/hudson/model/listeners/ItemListenerTest.java
@@ -23,21 +23,15 @@
*/
package hudson.model.listeners;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-
-import hudson.cli.CLI;
+import hudson.cli.CLICommandInvoker;
import hudson.model.Item;
+import java.io.ByteArrayInputStream;
+import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
-import java.util.Arrays;
-
/**
* Tests for ItemListener events.
* @author Alan.Harder@sun.com
@@ -64,16 +58,11 @@ public class ItemListenerTest {
@Test
public void onCreatedViaCLI() throws Exception {
- ByteArrayOutputStream buf = new ByteArrayOutputStream();
- PrintStream out = new PrintStream(buf);
- try (CLI cli = new CLI(j.getURL())) {
- cli.execute(Arrays.asList("create-job", "testJob"),
- new ByteArrayInputStream((""
- + "").getBytes()),
- out, out);
- out.flush();
- assertNotNull("job should be created: " + buf, j.jenkins.getItem("testJob"));
- assertEquals("onCreated event should be triggered: " + buf, "C", events.toString());
- }
+ CLICommandInvoker.Result result = new CLICommandInvoker(j, "create-job").
+ withStdin(new ByteArrayInputStream(("").getBytes())).
+ invokeWithArgs("testJob");
+ assertThat(result, CLICommandInvoker.Matcher.succeeded());
+ assertNotNull("job should be created: " + result, j.jenkins.getItem("testJob"));
+ assertEquals("onCreated event should be triggered: " + result, "C", events.toString());
}
}
diff --git a/test/src/test/java/hudson/security/CliAuthenticationTest.java b/test/src/test/java/hudson/security/CliAuthenticationTest.java
index 52b17fc9ebfe3bb4bf1906ff276a4b0c9ffe3acc..bdddf14407b1058805b1ed8b0b86832846ebf424 100644
--- a/test/src/test/java/hudson/security/CliAuthenticationTest.java
+++ b/test/src/test/java/hudson/security/CliAuthenticationTest.java
@@ -7,9 +7,6 @@ import static org.junit.Assert.assertTrue;
import hudson.cli.CLI;
import hudson.cli.CLICommand;
-import hudson.cli.ClientAuthenticationCache;
-import hudson.cli.LoginCommand;
-import hudson.cli.LogoutCommand;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.apache.commons.io.input.NullInputStream;
@@ -25,6 +22,7 @@ import java.util.Arrays;
/**
* @author Kohsuke Kawaguchi
*/
+@SuppressWarnings("deprecation") // Remoting-based CLI usages intentional
public class CliAuthenticationTest {
@Rule
@@ -88,7 +86,7 @@ public class CliAuthenticationTest {
}
@Test
- @For({LoginCommand.class, LogoutCommand.class, ClientAuthenticationCache.class})
+ @For({hudson.cli.LoginCommand.class, hudson.cli.LogoutCommand.class, hudson.cli.ClientAuthenticationCache.class})
public void login() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
diff --git a/test/src/test/java/jenkins/CLITest.java b/test/src/test/java/jenkins/CLITest.java
index 054d4df5a113d0e00ee9dbcbc08287aee6318d39..43c8be4cf082cdde0dc5505106f04707b83f0324 100644
--- a/test/src/test/java/jenkins/CLITest.java
+++ b/test/src/test/java/jenkins/CLITest.java
@@ -1,21 +1,11 @@
package jenkins;
-import hudson.cli.FullDuplexHttpStream;
-import hudson.model.Computer;
import hudson.model.Failure;
-import hudson.remoting.Channel;
+import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
-import java.io.FileNotFoundException;
-import java.net.URL;
-
-import static org.junit.Assert.*;
-
-/**
- * @author Kohsuke Kawaguchi
- */
public class CLITest {
@Rule
public JenkinsRule j = new JenkinsRule();
@@ -26,39 +16,35 @@ public class CLITest {
@Test
public void killSwitch() throws Exception {
// this should succeed, as a control case
- makeHttpCall();
- makeJnlpCall();
+ j.jenkins.setSlaveAgentPort(-1); // force HTTP connection
+ makeCall();
+ j.jenkins.setSlaveAgentPort(0); // allow TCP connection
+ makeCall();
- CLI.DISABLED = true;
+ CLI.get().setEnabled(false);
try {
- try {
- makeHttpCall();
- fail("Should have been rejected");
- } catch (FileNotFoundException e) {
- // attempt to make a call should fail
- }
- try {
- makeJnlpCall();
- fail("Should have been rejected");
- } catch (Exception e) {
- // attempt to make a call should fail
- e.printStackTrace();
-
- // the current expected failure mode is EOFException, though we don't really care how it fails
- }
- } finally {
- CLI.DISABLED = false;
+ j.jenkins.setSlaveAgentPort(-1);
+ makeCall();
+ fail("Should have been rejected");
+ } catch (Exception e) {
+ // attempt to make a call should fail
+ e.printStackTrace();
+ // currently sends a 403
+ }
+ try {
+ j.jenkins.setSlaveAgentPort(0);
+ makeCall();
+ fail("Should have been rejected");
+ } catch (Exception e) {
+ // attempt to make a call should fail
+ e.printStackTrace();
+
+ // the current expected failure mode is EOFException, though we don't really care how it fails
}
}
- private void makeHttpCall() throws Exception {
- FullDuplexHttpStream con = new FullDuplexHttpStream(new URL(j.getURL(), "cli"));
- Channel ch = new Channel("test connection", Computer.threadPoolForRemoting, con.getInputStream(), con.getOutputStream());
- ch.close();
- }
-
- private void makeJnlpCall() throws Exception {
- int r = hudson.cli.CLI._main(new String[]{"-s",j.getURL().toString(), "version"});
+ private void makeCall() throws Exception {
+ int r = hudson.cli.CLI._main(new String[] {"-s", j.getURL().toString(), "-remoting", "-noKeyAuth", "version"});
if (r!=0)
throw new Failure("CLI failed");
}
diff --git a/test/src/test/java/jenkins/security/Security218BlackBoxTest.java b/test/src/test/java/jenkins/security/Security218BlackBoxTest.java
index 4776ee92e42bf627a844c5b5567352fe7e0137a3..b420978b11511bc21ad06da770f5a27129998921 100644
--- a/test/src/test/java/jenkins/security/Security218BlackBoxTest.java
+++ b/test/src/test/java/jenkins/security/Security218BlackBoxTest.java
@@ -85,7 +85,7 @@ public class Security218BlackBoxTest {
@Rule
public JenkinsRule r = new JenkinsRule();
- @SuppressWarnings("deprecation") // really mean to use getPage(String)
+ @SuppressWarnings("deprecation") // really mean to use getPage(String), and Remoting-based CLI
@PresetData(PresetData.DataSet.ANONYMOUS_READONLY) // TODO userContent inaccessible without authentication otherwise
@Test
public void probe() throws Exception {
@@ -277,7 +277,7 @@ public class Security218BlackBoxTest {
try {
CLI cli = new CLI(r.getURL()) {
@Override
- protected CliPort getCliTcpPort(String url) throws IOException {
+ protected CliPort getCliTcpPort(URL url) throws IOException {
return new CliPort(new InetSocketAddress(proxySocket.getInetAddress(), proxySocket.getLocalPort()), /* ignore identity */ null, 1);
}
};
diff --git a/test/src/test/java/jenkins/security/Security218CliTest.java b/test/src/test/java/jenkins/security/Security218CliTest.java
index f9f39c4d46d3388a1039d6275c6d0d18f11161a0..cfa0002d7114389c8887a94c8218ff557d0bfc06 100644
--- a/test/src/test/java/jenkins/security/Security218CliTest.java
+++ b/test/src/test/java/jenkins/security/Security218CliTest.java
@@ -41,6 +41,7 @@ import org.jvnet.hudson.test.TestExtension;
import org.jvnet.hudson.test.recipes.PresetData;
import org.kohsuke.args4j.Argument;
+@SuppressWarnings("deprecation") // Remoting-based CLI usages intentional
public class Security218CliTest {
@Rule
@@ -197,8 +198,8 @@ public class Security218CliTest {
@Argument(metaVar = "command", usage = "Command to be launched by the payload", required = true, index = 1)
public String command;
-
+ @Override
protected int run() throws Exception {
Payload payloadItem = Payload.valueOf(this.payload);
PayloadCaller callable = new PayloadCaller(payloadItem, command);
diff --git a/test/src/test/java/jenkins/security/Security232Test.java b/test/src/test/java/jenkins/security/Security232Test.java
index f5a33ab6ab6ff1f94e7024f1ccb912f771264559..65e0ea05d48d4974a6109b9f1930f30cc04cbc7e 100644
--- a/test/src/test/java/jenkins/security/Security232Test.java
+++ b/test/src/test/java/jenkins/security/Security232Test.java
@@ -28,10 +28,10 @@ import java.rmi.activation.ActivationInstantiator;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObject;
import java.rmi.server.UnicastRemoteObject;
+import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.net.SocketFactory;
-import static jenkins.security.security218.Payload.CommonsCollections1;
import jenkins.security.security218.ysoserial.payloads.CommonsCollections1;
import jenkins.security.security218.ysoserial.payloads.ObjectPayload;
import static org.junit.Assert.*;
@@ -59,6 +59,8 @@ public class Security232Test {
@Test
public void commonsCollections1() throws Exception {
+ r.jenkins.setAgentProtocols(Collections.singleton("CLI-connect")); // override CliProtocol.OPT_IN
+
File pwned = new File(r.jenkins.getRootDir(), "pwned");
int jrmpPort = 12345;
diff --git a/test/src/test/resources/hudson/cli/id_rsa b/test/src/test/resources/hudson/cli/id_rsa
new file mode 100644
index 0000000000000000000000000000000000000000..ee2ad6b569a3b5ebbea0d5d1bfcf6ed2bd0dbe27
--- /dev/null
+++ b/test/src/test/resources/hudson/cli/id_rsa
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAyTqwFqp5Ww2Tr/52D7hhdOwgzYGBUqxrOFopa+kjNEL1Yqwb
++mApUWZ+D3zN9PurhUcVUfeYVXiYWFJ0kG72HIJawL/0BR5oYxRJfumK8Z/sAzAL
+xdhc5O5twETrr9gU3cxtvF5oJNP0I9HickAOeC+ZNpiDIIblrhvxXl/QwqrR+/Gv
+Nb8TApj+rxXEfNp+N69iGnnxzWn1FeKeOAWpwoBAxZNoqBQAFacF7xfQnoygyekC
+xk+ts2O5Zzv8iJ10sVf+x2Q79rxAtsc0xOGhZbBAzbmFTz0PE4iWuo/Vo1c6mM7u
+/dam+FxB2NqPNw7W+4eiCnEVkiQZlrxmuGvK7wIDAQABAoIBACml1+QZDFzoBnUa
+eVzvkFwesvtVnmp5/QcAwinvarXaVedCL9g2JtcOG3EhJ49YtzsyZxs7329xMja1
+eiKalJ157UaPc/XLQVegT0XRGEzCCJrwSr979F39awGsQgt28XqmYN/nui5FH/Z5
+7iAvWc9OKqu+DQWiZc8PQXmC4zYmvhGQ8vKx44RSqlWCjd9IqBVhpE5gxpI/SmCx
+umUNNtoH0hBWr+MsVHzr6UUrC3a99+7bB4We8XMXXFLzbTUSgiYFmK+NxPs/Fux/
+IAyXAMbDw2HeqZ7g4kTaf4cvmVOwhh4zlvB4p7j301LdO1jmvs9z0fn/QJcTpVM7
+ISMKwAECgYEA/uKVdmOKTk3dKzKRFXtWJjqypOXakoX+25lUcVv2PXYRr8Sln9jC
+A13fbhvwq+FqbdnNlB23ag5niCVLfUpB1DYYP5jd4lU8D6HZQiHlmokB6nLT9NIW
+iTcG88E58Bta/l1Ue5Yn+LqluBC4i289wFbH1kZyxQ565s5dJEv9uAECgYEAyhwF
+ZOqTK2lZe5uuN4owVLQaYFj9fsdFHULzlK/UAtkG1gCJhjBmwSEpZFFMH6WgwHk5
+SHJEom0uB4qRv8gQcxl9OSiDsp56ymr0NBhlPVXWr6IzLotLy5XBC1muqvYYlj7E
+kHgSet/h8RUM/FeEiwOFHDU2DkMb8Qx1hfMdAu8CgYBSEsYL9CuB4WK5WTQMlcV8
+0+PYY0dJbSpOrgXZ5sHYsp8pWQn3+cUnbl/WxdpujkxGCR9AdX0tAmxmE5RGSNX/
+rleKiv/PtKB9bCFYQS/83ecnBkioCcpF7tknPm4YmcZoJ8dfcE94sSlRpti11WEu
+AQOiRNcKCwqaLZMib/HIAQKBgQCdiOffeERMYypfgcJzAiCX9WZV0SeOCS7jFwub
+ys17hsSgS/zl/pYpVXrY+dFXHZfGTvcKdB7xaB6nvCfND9lajfSgd+bndEYLvwAo
+Fxfajizv64LvdZ4XytuUyEuwcHBLtBMs9Jqa8iU/8AOWMXVbkdvQV92RkleWNPrp
+9MyZOwKBgQD9x8MnX5LVBfQKuL9qX6l9Da06EyMkzfz3obKn9AAJ3Xj9+45TNPJu
+HnZyvJWesl1vDjXQTm+PVkdyE0WQgoiVX+wxno0hsoly5Uqb5EYHtTUrZzRpkyLK
+1VmtDxT5D8gorUgn6crzk4PKaxRkPfAimZdlkQm6iOtuR3kqn5BtIQ==
+-----END RSA PRIVATE KEY-----
diff --git a/test/src/test/resources/hudson/cli/id_rsa.pub b/test/src/test/resources/hudson/cli/id_rsa.pub
new file mode 100644
index 0000000000000000000000000000000000000000..91f8ff7180e5f05f989b9aec2104d102372e88a4
--- /dev/null
+++ b/test/src/test/resources/hudson/cli/id_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJOrAWqnlbDZOv/nYPuGF07CDNgYFSrGs4Wilr6SM0QvVirBv6YClRZn4PfM30+6uFRxVR95hVeJhYUnSQbvYcglrAv/QFHmhjFEl+6Yrxn+wDMAvF2Fzk7m3AROuv2BTdzG28Xmgk0/Qj0eJyQA54L5k2mIMghuWuG/FeX9DCqtH78a81vxMCmP6vFcR82n43r2IaefHNafUV4p44BanCgEDFk2ioFAAVpwXvF9CejKDJ6QLGT62zY7lnO/yInXSxV/7HZDv2vEC2xzTE4aFlsEDNuYVPPQ8TiJa6j9WjVzqYzu791qb4XEHY2o83Dtb7h6IKcRWSJBmWvGa4a8rv your_email@example.com
diff --git a/war/pom.xml b/war/pom.xml
index 765bbc9b8a7d97208251bd6741930d16cf367b61..24c79dd05025ce2e15868b98be48cc9972278400 100644
--- a/war/pom.xml
+++ b/war/pom.xml
@@ -134,7 +134,7 @@ THE SOFTWARE.
org.jenkins-ci.modules
sshd
- 1.10
+ 1.11
org.jenkins-ci.ui
diff --git a/war/src/main/webapp/help/parameter/file.html b/war/src/main/webapp/help/parameter/file.html
index 54866f662c521c84bf84157f5c0a5d386abfaed9..22b0a928c76701011fdb802e808b27e9b1305266 100644
--- a/war/src/main/webapp/help/parameter/file.html
+++ b/war/src/main/webapp/help/parameter/file.html
@@ -21,4 +21,10 @@
anything (but it also will not delete anything that's already in the
workspace.)
+
+ From the CLI, the -p
option to the build
command
+ can take either a local filename (-remoting
mode only),
+ or an empty value to read from standard input.
+ (In the latter mode, only one file parameter can be defined.)
+
\ No newline at end of file