提交 72901698 编写于 作者: J Jesse Glick

[FIXED JENKINS-41745] Merged #2795: non-Remoting-based CLI.

......@@ -34,6 +34,10 @@
<artifactId>commons-codec</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>remoting</artifactId>
......@@ -50,6 +54,17 @@
<version>1.24</version>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId>
<version>1.2.0</version> <!-- TODO 1.3.0 requires Java 8 -->
<optional>true</optional> <!-- do not expose to core -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<optional>true</optional> <!-- ditto -->
</dependency>
<dependency> <!-- TODO remove and replace PrivateKeyProvider with SecurityUtils.createFileKeyPairProvider() as in SshClient -->
<groupId>org.jenkins-ci</groupId>
<artifactId>trilead-ssh2</artifactId>
<version>build214-jenkins-1</version>
......
......@@ -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<String> 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<String> 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<KeyPair> 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());
}
......@@ -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);
}
......
......@@ -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.
......
......@@ -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.
......
......@@ -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;
......
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();
}
}
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;
}
}
}
}
......@@ -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 {
......
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<len; i++) {
byte b = buf[start+i];
if (b >= 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();
}
}
/*
* 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() {}
}
/*
* 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<String> 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<ClientChannelEvent> 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() {}
}
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;
}
......@@ -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.
......
/*
* 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);
}
}
......@@ -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),
......
......@@ -772,7 +772,6 @@ THE SOFTWARE.
<forkCount>0.5C</forkCount>
<reuseForks>true</reuseForks>
<argLine>-noverify</argLine> <!-- some versions of JDK7/8 causes VerifyError during mock tests: http://code.google.com/p/powermock/issues/detail?id=504 -->
<trimStackTrace>false</trimStackTrace> <!-- SUREFIRE-1226 workaround -->
</configuration>
</plugin>
<plugin><!-- set main class -->
......
......@@ -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<UUID,FullDuplexHttpChannel> duplexChannels = new HashMap<UUID, FullDuplexHttpChannel>();
private static final Logger LOGGER = Logger.getLogger(CLIAction.class.getName());
private transient final Map<UUID, FullDuplexHttpService> 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<Thread> runningThread = new AtomicReference<>();
class ServerSideImpl extends PlainCLIProtocol.ServerSide {
boolean ready;
List<String> 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));
}
};
}
}
}
......@@ -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 {
* <p>
* 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));
}
......
......@@ -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;
......
......@@ -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;
}
/**
......
......@@ -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;
}
/**
......
......@@ -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.
......
......@@ -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.
......
......@@ -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.
......
......@@ -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()) {
......
......@@ -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)
......
......@@ -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<String> sources = new ArrayList<String>();
@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);
......
......@@ -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'")
......
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.
......
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());
......
......@@ -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")
......
......@@ -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)
......
......@@ -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<String,IOException> {
private final String script;
......
......@@ -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;
......
......@@ -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.
*
* <p>
* 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()<end)
wait(1000);
if (upload==null)
throw new IOException("HTTP full-duplex channel timeout: "+uuid);
}
try {
channel = new Channel("HTTP full-duplex channel " + uuid,
Computer.threadPoolForRemoting, Mode.BINARY, upload, out, null, restricted);
// so that we can detect dead clients, periodically send something
PingThread ping = new PingThread(channel) {
@Override
protected void onDead(Throwable diagnosis) {
LOGGER.log(Level.INFO,"Duplex-HTTP session " + uuid + " is terminated",diagnosis);
// this will cause the channel to abort and subsequently clean up
try {
upload.close();
} catch (IOException e) {
// this can never happen
throw new AssertionError(e);
}
}
@Override
protected void onDead() {
onDead(null);
@Override
protected void run(final InputStream upload, OutputStream download) throws IOException, InterruptedException {
channel = new Channel("HTTP full-duplex channel " + uuid,
Computer.threadPoolForRemoting, Mode.BINARY, upload, download, null, restricted);
// so that we can detect dead clients, periodically send something
PingThread ping = new PingThread(channel) {
@Override
protected void onDead(Throwable diagnosis) {
LOGGER.log(Level.INFO, "Duplex-HTTP session " + uuid + " is terminated", diagnosis);
// this will cause the channel to abort and subsequently clean up
try {
upload.close();
} catch (IOException e) {
// this can never happen
throw new AssertionError(e);
}
};
ping.start();
main(channel);
channel.join();
ping.interrupt();
} finally {
// publish that we are done
completed=true;
notify();
}
}
@Override
protected void onDead() {
onDead(null);
}
};
ping.start();
main(channel);
channel.join();
ping.interrupt();
}
protected abstract void main(Channel channel) 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();
}
public Channel getChannel() {
return channel;
}
private static final Logger LOGGER = Logger.getLogger(FullDuplexHttpChannel.class.getName());
/**
* Set to true if the servlet container doesn't support chunked encoding.
*/
@Restricted(NoExternalUse.class)
public static boolean DIY_CHUNKING = SystemProperties.getBoolean("hudson.diyChunking");
/**
* Controls the time out of waiting for the 2nd HTTP request to arrive.
*/
@Restricted(NoExternalUse.class)
public static long CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(15);
}
......@@ -194,8 +194,7 @@ public abstract class ParameterDefinition implements
* Create a parameter value from the string given in the CLI.
*
* @param command
* This is the command that got the parameter. You can use its {@link CLICommand#checkChannel()}
* for interacting with the CLI JVM.
* This is the command that got the parameter.
* @throws AbortException
* If the CLI processing should be aborted. Hudson will report the error message
* without stack trace, and then exits this command. Useful for graceful termination.
......
......@@ -50,6 +50,7 @@ public abstract class AbstractPasswordBasedSecurityRealm extends SecurityRealm i
new ImpersonatingUserDetailsService(this));
}
@Deprecated
@Override
public CliAuthenticator createCliAuthenticator(final CLICommand command) {
return new CliAuthenticator() {
......@@ -68,12 +69,12 @@ public abstract class AbstractPasswordBasedSecurityRealm extends SecurityRealm i
if (passwordFile!=null)
try {
password = new FilePath(command.channel,passwordFile).readToString().trim();
password = new FilePath(command.checkChannel(), passwordFile).readToString().trim();
} catch (IOException e) {
throw new BadCredentialsException("Failed to read "+passwordFile,e);
}
if (password==null)
password = command.channel.call(new InteractivelyAskForPassword());
password = command.checkChannel().call(new InteractivelyAskForPassword());
if (password==null)
throw new BadCredentialsException("No password specified");
......
......@@ -75,7 +75,9 @@ import java.io.IOException;
*
* @author Kohsuke Kawaguchi
* @since 1.350
* @deprecated Vulnerable to JENKINS-12543.
*/
@Deprecated
public abstract class CliAuthenticator {
/**
* Authenticates the CLI invocation. See class javadoc for the semantics.
......
......@@ -189,7 +189,9 @@ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityReal
* @return
* never null. By default, this method returns a no-op authenticator that always authenticates
* the session as authenticated by the transport (which is often just {@link jenkins.model.Jenkins#ANONYMOUS}.)
* @deprecated See {@link CliAuthenticator}.
*/
@Deprecated
public CliAuthenticator createCliAuthenticator(final CLICommand command) {
return new CliAuthenticator() {
public Authentication authenticate() {
......
package jenkins;
import hudson.Extension;
import hudson.model.AdministrativeMonitor;
import java.io.IOException;
import javax.annotation.Nonnull;
import jenkins.model.GlobalConfiguration;
import jenkins.model.GlobalConfigurationCategory;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Kill switch to disable the entire Jenkins CLI system.
* Kill switch to disable the CLI-over-Remoting system.
*
* Marked as no external use because the CLI subsystem is nearing EOL.
*
* @author Kohsuke Kawaguchi
*/
@Restricted(NoExternalUse.class)
public class CLI {
// non-final to allow setting from $JENKINS_HOME/init.groovy.d
@Extension @Symbol("remotingCLI")
public class CLI extends GlobalConfiguration {
/**
* Supersedes {@link #isEnabled} if set.
* @deprecated Use {@link #setEnabled} instead.
*/
@Deprecated
public static boolean DISABLED = Boolean.getBoolean(CLI.class.getName()+".disabled");
@Nonnull
public static CLI get() {
CLI instance = GlobalConfiguration.all().get(CLI.class);
if (instance == null) {
// should not happen
return new CLI();
}
return instance;
}
private boolean enabled = true; // historical default, but overridden in SetupWizard
public CLI() {
load();
}
@Override
public GlobalConfigurationCategory getCategory() {
return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class);
}
public boolean isEnabled() {
return enabled && !DISABLED;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
save();
}
@Extension @Symbol("remotingCLI")
public static class WarnWhenEnabled extends AdministrativeMonitor {
public WarnWhenEnabled() {
super(CLI.class.getName());
}
@Override
public String getDisplayName() {
return "Remoting over CLI";
}
@Override
public boolean isActivated() {
return CLI.get().isEnabled();
}
@RequirePOST
public HttpResponse doAct(@QueryParameter String no) throws IOException {
if (no == null) {
CLI.get().setEnabled(false);
} else {
disable(true);
}
return HttpResponses.redirectViaContextPath("manage");
}
}
}
......@@ -53,6 +53,7 @@ import java.net.URL;
import java.net.URLConnection;
import java.util.Iterator;
import java.util.List;
import jenkins.CLI;
import jenkins.model.Jenkins;
import jenkins.security.s2m.AdminWhitelistRule;
......@@ -121,6 +122,9 @@ public class SetupWizard extends PageDecorator {
// Disable jnlp by default, but honor system properties
jenkins.setSlaveAgentPort(SystemProperties.getInteger(Jenkins.class.getName()+".slaveAgentPort",-1));
// Disable CLI over Remoting
CLI.get().setEnabled(false);
// require a crumb issuer
jenkins.setCrumbIssuer(new DefaultCrumbIssuer(false));
......
/*
* 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 jenkins.util;
import hudson.cli.FullDuplexHttpStream;
import hudson.util.ChunkedInputStream;
import hudson.util.ChunkedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
/**
* Server-side counterpart to {@link FullDuplexHttpStream}.
* @since 2.54
*/
public abstract class FullDuplexHttpService {
/**
* Set to true if the servlet container doesn't support chunked encoding.
*/
@Restricted(NoExternalUse.class)
public static boolean DIY_CHUNKING = SystemProperties.getBoolean("hudson.diyChunking");
/**
* Controls the time out of waiting for the 2nd HTTP request to arrive.
*/
@Restricted(NoExternalUse.class)
public static long CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(15);
protected final UUID uuid;
private InputStream upload;
private boolean completed;
protected FullDuplexHttpService(UUID uuid) {
this.uuid = uuid;
}
/**
* This is where we send the data to the client.
*
* <p>
* 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<UUID, FullDuplexHttpService> services;
/**
* @param services a cross-request cache of services, to correlate the
* upload and download connections
*/
protected Response(Map<UUID, FullDuplexHttpService> 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;
}
}
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=\
......
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<div class="warning">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<div style="float:right">
<f:submit name="yes" value="${%Disable CLI over Remoting}"/>
<f:submit name="no" value="${%Dismiss}"/>
</div>
${%blurb}
</form>
</div>
</j:jelly>
# 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 <code>-remoting</code> mode is considered dangerous and usually unnecessary. \
You are advised to disable this mode. \
Please refer to the <a href="https://jenkins.io/doc/book/managing/cli/">CLI documentation</a> for details.
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:section title="${%CLI}">
<f:entry field="enabled">
<f:checkbox title="${%Enable CLI over Remoting}"/>
</f:entry>
</f:section>
</j:jelly>
<div>
Whether to enable the historical Jenkins CLI mode over remoting
(<code>-remoting</code> option in the client).
While this may be necessary to support certain commands or command options,
it is considered intrinsically insecure.
(<code>-http</code> mode is always available,
and <code>-ssh</code> mode is available whenever the SSH service is enabled.)
</div>
......@@ -108,12 +108,7 @@ THE SOFTWARE.
<repository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<!-- allow snapshots -->
</repository>
</repositories>
......@@ -121,12 +116,6 @@ THE SOFTWARE.
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
......@@ -202,11 +191,6 @@ THE SOFTWARE.
<artifactId>slf4j-api</artifactId>
<version>${slf4jVersion}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>${slf4jVersion}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
......@@ -392,6 +376,7 @@ THE SOFTWARE.
<forkedProcessTimeoutInSeconds>3600</forkedProcessTimeoutInSeconds>
<java.awt.headless>true</java.awt.headless>
</systemPropertyVariables>
<trimStackTrace>false</trimStackTrace> <!-- SUREFIRE-1226 workaround -->
</configuration>
</plugin>
<plugin>
......
......@@ -40,7 +40,6 @@ THE SOFTWARE.
<concurrency>2</concurrency> <!-- may use e.g. 2C for 2 × (number of cores) -->
<mavenDebug>false</mavenDebug>
<jacocoSurefireArgs /><!-- empty by default -->
<trimStackTrace>false</trimStackTrace> <!-- SUREFIRE-1226 workaround -->
</properties>
<dependencies>
......@@ -53,12 +52,6 @@ THE SOFTWARE.
<artifactId>jenkins-war</artifactId>
<version>${project.version}</version>
<classifier>war-for-test</classifier>
<exclusions>
<exclusion>
<groupId>org.jenkins-ci.modules</groupId>
<artifactId>sshd</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
......
......@@ -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<ParameterDefinition> pd = [new StringParameterDefinition("a", ""), new StringParameterDefinition("b", "")];
p.addProperty(new ParametersDefinitionProperty(pd))
p.buildersList.add(createScriptBuilder("java -jar cli.jar set-build-parameter a b"))
p.buildersList.add(createScriptBuilder("java -jar cli.jar set-build-parameter a x"))
p.buildersList.add(createScriptBuilder("java -jar cli.jar set-build-parameter b y"))
p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b"))
p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a x"))
p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter b y"))
def r = [:];
......@@ -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"]);
......
/*
* 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);
}
}
......@@ -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<String> 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;
}
}
}
/*
* 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<String> 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());
}
}
......@@ -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();
......
/*
* 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"));
}
}
......@@ -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
......
......@@ -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(("<project><actions/><builders/><publishers/>"
+ "<buildWrappers/></project>").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(("<project><actions/><builders/><publishers/><buildWrappers/></project>").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());
}
}
......@@ -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());
......
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");
}
......
......@@ -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);
}
};
......
......@@ -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);
......
......@@ -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;
......
-----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-----
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJOrAWqnlbDZOv/nYPuGF07CDNgYFSrGs4Wilr6SM0QvVirBv6YClRZn4PfM30+6uFRxVR95hVeJhYUnSQbvYcglrAv/QFHmhjFEl+6Yrxn+wDMAvF2Fzk7m3AROuv2BTdzG28Xmgk0/Qj0eJyQA54L5k2mIMghuWuG/FeX9DCqtH78a81vxMCmP6vFcR82n43r2IaefHNafUV4p44BanCgEDFk2ioFAAVpwXvF9CejKDJ6QLGT62zY7lnO/yInXSxV/7HZDv2vEC2xzTE4aFlsEDNuYVPPQ8TiJa6j9WjVzqYzu791qb4XEHY2o83Dtb7h6IKcRWSJBmWvGa4a8rv your_email@example.com
......@@ -134,7 +134,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci.modules</groupId>
<artifactId>sshd</artifactId>
<version>1.10</version>
<version>1.11</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.ui</groupId>
......
......@@ -21,4 +21,10 @@
anything (but it also will not delete anything that's already in the
workspace.)
</p>
<p>
From the CLI, the <code>-p</code> option to the <code>build</code> command
can take either a local filename (<code>-remoting</code> mode only),
or an empty value to read from standard input.
(In the latter mode, only one file parameter can be defined.)
</p>
</div>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册