Permalink
Browse files

[JENKINS-36871, JENKINS-37565] JNLP4-connect implementation and Remot…

…ing 3 (#2492)

* [JENKINS-36871] Switch to the new JnlpProtocolHandler based implementation

Todo

- [ ] Restore the cookie behaviour (but done right this time)
- [ ] Perhaps investigate issuing clients with TLS certificates (but would require a UI for managing them)

* [JENKINS-36871] License headers and javadocs

* [JENKINS-36871] Restore cookie handling

* [JENKINS-36871] Integrating Agent discovery components

* [JENKINS-36871] Pick up remoting 3.0-SNAPSHOT

* [JENKINS-36871] Pick up newer snapshot

* [JENKINS-36871] Oleg wants to log an exception that cannot happen
  • Loading branch information...
stephenc authored and oleg-nenashev committed Oct 20, 2016
1 parent 5537b31 commit 71cbe0cc7c601c04509faa618b23194335288fee
@@ -1,22 +1,31 @@
package jenkins.slaves;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.TcpSlaveAgentListener.ConnectionFromCurrentPeer;
import hudson.Util;
import hudson.model.Computer;
import hudson.model.Slave;
import hudson.remoting.Channel;
import hudson.slaves.JNLPLauncher;
import hudson.slaves.SlaveComputer;
import java.io.OutputStream;
import java.io.PrintWriter;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import org.jenkinsci.remoting.engine.JnlpServerHandshake;
import jenkins.security.ChannelConfigurator;
import org.apache.commons.io.IOUtils;
import org.jenkinsci.remoting.engine.JnlpConnectionState;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jenkinsci.remoting.protocol.impl.ConnectionRefusalException;
/**
* Match the name against the agent name and route the incoming JNLP agent as {@link Slave}.
@@ -27,81 +36,108 @@
*/
@Extension
public class DefaultJnlpSlaveReceiver extends JnlpAgentReceiver {
@Override
public boolean handle(String nodeName, JnlpServerHandshake handshake) throws IOException, InterruptedException {
SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName);
public boolean owns(String clientName) {
Computer computer = Jenkins.getInstance().getComputer(clientName);
return computer != null;
}
if (computer==null) {
return false;
@Override
public void afterProperties(@NonNull JnlpConnectionState event) {
String clientName = event.getProperty(JnlpConnectionState.CLIENT_NAME_KEY);
SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(clientName);
if (computer == null || !(computer.getLauncher() instanceof JNLPLauncher)) {
event.reject(new ConnectionRefusalException(String.format("%s is not a JNLP agent", clientName)));
return;
}
Channel ch = computer.getChannel();
if (ch !=null) {
String c = handshake.getRequestProperty("Cookie");
if (c!=null && c.equals(ch.getProperty(COOKIE_NAME))) {
if (ch != null) {
String cookie = event.getProperty(JnlpConnectionState.COOKIE_KEY);
if (cookie != null && cookie.equals(ch.getProperty(COOKIE_NAME))) {
// we think we are currently connected, but this request proves that it's from the party
// we are supposed to be communicating to. so let the current one get disconnected
LOGGER.info("Disconnecting "+nodeName+" as we are reconnected from the current peer");
LOGGER.log(Level.INFO, "Disconnecting {0} as we are reconnected from the current peer", clientName);
try {
computer.disconnect(new ConnectionFromCurrentPeer()).get(15, TimeUnit.SECONDS);
} catch (ExecutionException | TimeoutException e) {
throw new IOException("Failed to disconnect the current client",e);
} catch (ExecutionException | TimeoutException | InterruptedException e) {
event.reject(new ConnectionRefusalException("Failed to disconnect the current client", e));
return;
}
} else {
handshake.error(nodeName + " is already connected to this master. Rejecting this connection.");
return true;
event.reject(new ConnectionRefusalException(String.format(
"%s is already connected to this master. Rejecting this connection.", clientName)));
return;
}
}
event.approve();
event.setStash(new State(computer));
}
@Override
public void beforeChannel(@NonNull JnlpConnectionState event) {
DefaultJnlpSlaveReceiver.State state = event.getStash(DefaultJnlpSlaveReceiver.State.class);
final SlaveComputer computer = state.getNode();
final OutputStream log = computer.openLogFile();
state.setLog(log);
PrintWriter logw = new PrintWriter(log, true);
logw.println("JNLP agent connected from " + event.getSocket().getInetAddress());
for (ChannelConfigurator cc : ChannelConfigurator.all()) {
cc.onChannelBuilding(event.getChannelBuilder(), computer);
}
event.getChannelBuilder().withHeaderStream(log);
String cookie = event.getProperty(JnlpConnectionState.COOKIE_KEY);
if (cookie != null) {
event.getChannelBuilder().withProperty(COOKIE_NAME, cookie);
}
}
if (!matchesSecret(nodeName,handshake)) {
handshake.error(nodeName + " can't be connected since the agent's secret does not match the handshake secret.");
return true;
@Override
public void afterChannel(@NonNull JnlpConnectionState event) {
DefaultJnlpSlaveReceiver.State state = event.getStash(DefaultJnlpSlaveReceiver.State.class);
final SlaveComputer computer = state.getNode();
try {
computer.setChannel(event.getChannel(), state.getLog(), null);
} catch (IOException | InterruptedException e) {
PrintWriter logw = new PrintWriter(state.getLog(), true);
e.printStackTrace(logw);
IOUtils.closeQuietly(event.getChannel());
}
}
Properties response = new Properties();
String cookie = generateCookie();
response.put("Cookie",cookie);
handshake.success(response);
@Override
public void channelClosed(@NonNull JnlpConnectionState event) {
final String nodeName = event.getProperty(JnlpConnectionState.CLIENT_NAME_KEY);
IOException cause = event.getCloseCause();
if (cause != null) {
LOGGER.log(Level.WARNING, Thread.currentThread().getName() + " for " + nodeName + " terminated",
cause);
}
}
// this cast is leaking abstraction
JnlpSlaveAgentProtocol2.Handler handler = (JnlpSlaveAgentProtocol2.Handler)handshake;
private static class State implements JnlpConnectionState.ListenerState {
@Nonnull
private final SlaveComputer node;
@CheckForNull
private OutputStream log;
ch = handler.jnlpConnect(computer);
public State(@Nonnull SlaveComputer node) {
this.node = node;
}
ch.setProperty(COOKIE_NAME, cookie);
@Nonnull
public SlaveComputer getNode() {
return node;
}
return true;
}
/**
* Called after the client has connected to check if the agent secret matches the handshake secret
*
* @param nodeName
* Name of the incoming JNLP agent. All {@link JnlpAgentReceiver} shares a single namespace
* of names. The implementation needs to be able to tell which name belongs to them.
*
* @param handshake
* Encapsulation of the interaction with the incoming JNLP agent.
*
* @return
* true if the agent secret matches the handshake secret, false otherwise.
*/
private boolean matchesSecret(String nodeName, JnlpServerHandshake handshake){
SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName);
String handshakeSecret = handshake.getRequestProperty("Secret-Key");
// Verify that the agent secret matches the handshake secret.
if (!computer.getJnlpMac().equals(handshakeSecret)) {
LOGGER.log(Level.WARNING, "An attempt was made to connect as {0} from {1} with an incorrect secret", new Object[]{nodeName, handshake.getSocket()!=null?handshake.getSocket().getRemoteSocketAddress():null});
return false;
} else {
return true;
@CheckForNull
public OutputStream getLog() {
return log;
}
}
private String generateCookie() {
byte[] cookie = new byte[32];
new SecureRandom().nextBytes(cookie);
return Util.toHexString(cookie);
public void setLog(@Nonnull OutputStream log) {
this.log = log;
}
}
private static final Logger LOGGER = Logger.getLogger(DefaultJnlpSlaveReceiver.class.getName());
@@ -0,0 +1,71 @@
/*
* The MIT License
*
* Copyright (c) 2016, 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.slaves;
import hudson.Extension;
import hudson.init.Terminator;
import hudson.model.Computer;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jenkinsci.remoting.protocol.IOHub;
/**
* Singleton holder of {@link IOHub}
*
* @since FIXME
*/
@Extension
public class IOHubProvider {
/**
* Our logger.
*/
private static final Logger LOGGER = Logger.getLogger(IOHubProvider.class.getName());
/**
* Our hub.
*/
private IOHub hub;
public IOHubProvider() {
try {
hub = IOHub.create(Computer.threadPoolForRemoting);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to launch IOHub", e);
this.hub = null;
}
}
public IOHub getHub() {
return hub;
}
@Terminator
public void cleanUp() throws IOException {
if (hub != null) {
hub.close();
hub = null;
}
}
}
@@ -2,15 +2,15 @@
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.Util;
import hudson.model.Slave;
import jenkins.model.Jenkins;
import org.jenkinsci.remoting.engine.JnlpServerHandshake;
import java.io.IOException;
import java.util.Properties;
import java.security.SecureRandom;
import javax.annotation.Nonnull;
import org.jenkinsci.remoting.engine.JnlpClientDatabase;
import org.jenkinsci.remoting.engine.JnlpConnectionStateListener;
/**
* Receives incoming agents connecting through {@link JnlpSlaveAgentProtocol2}.
* Receives incoming agents connecting through {@link JnlpSlaveAgentProtocol2}, {@link JnlpSlaveAgentProtocol3}, {@link JnlpSlaveAgentProtocol4}.
*
* <p>
* This is useful to establish the communication with other JVMs and use them
@@ -19,56 +19,42 @@
* @author Kohsuke Kawaguchi
* @since 1.561
*/
public abstract class JnlpAgentReceiver implements ExtensionPoint {
/**
* Called after the client has connected.
*
* <p>
* The implementation must do the following in the order:
*
* <ol>
* <li>Check if the implementation recognizes and claims the given name.
* If not, return false to let other {@link JnlpAgentReceiver} have a chance to
* take this connection.
*
* <li>If you claim the name but the connection is refused, call
* {@link JnlpSlaveHandshake#error(String)} to refuse the client, and return true.
* The connection will be shut down and the client will report this error to the user.
*
* <li>If you claim the name and the connection is OK, call
* {@link JnlpSlaveHandshake#success(Properties)} to accept the client.
*
* <li>Proceed to build a channel with {@link JnlpSlaveHandshake#createChannelBuilder(String)}
* and return true
*
* @param name
* Name of the incoming JNLP agent. All {@link JnlpAgentReceiver} shares a single namespace
* of names. The implementation needs to be able to tell which name belongs to them.
*
* @param handshake
* Encapsulation of the interaction with the incoming JNLP agent.
*
* @return
* true if the name was claimed and the handshake was completed (either successfully or unsuccessfully)
* false if the name was not claimed. Other {@link JnlpAgentReceiver}s will be called to see if they
* take this connection.
*
* @throws Exception
* Any exception thrown from this method will fatally terminate the connection.
*/
public abstract boolean handle(String name, JnlpServerHandshake handshake) throws IOException, InterruptedException;
public abstract class JnlpAgentReceiver extends JnlpConnectionStateListener implements ExtensionPoint {
/**
* @deprecated
* Use {@link #handle(String, JnlpServerHandshake)}
*/
public boolean handle(String name, JnlpSlaveHandshake handshake) throws IOException, InterruptedException {
return handle(name,(JnlpServerHandshake)handshake);
}
private static final SecureRandom secureRandom = new SecureRandom();
public static final JnlpClientDatabase DATABASE = new JnlpAgentDatabase();
public static ExtensionList<JnlpAgentReceiver> all() {
return ExtensionList.lookup(JnlpAgentReceiver.class);
}
public static boolean exists(String clientName) {
for (JnlpAgentReceiver receiver : all()) {
if (receiver.owns(clientName)) {
return true;
}
}
return false;
}
protected abstract boolean owns(String clientName);
public static String generateCookie() {
byte[] cookie = new byte[32];
secureRandom.nextBytes(cookie);
return Util.toHexString(cookie);
}
private static class JnlpAgentDatabase extends JnlpClientDatabase {
@Override
public boolean exists(String clientName) {
return JnlpAgentReceiver.exists(clientName);
}
@Override
public String getSecretOf(@Nonnull String clientName) {
return JnlpSlaveAgentProtocol.SLAVE_SECRET.mac(clientName);
}
}
}
Oops, something went wrong.

0 comments on commit 71cbe0c

Please sign in to comment.