package jenkins.slaves; import hudson.Extension; import hudson.TcpSlaveAgentListener.ConnectionFromCurrentPeer; import hudson.Util; import hudson.model.Slave; import hudson.remoting.Channel; import hudson.slaves.SlaveComputer; import jenkins.model.Jenkins; import org.jenkinsci.remoting.engine.JnlpServerHandshake; 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; /** * Match the name against the agent name and route the incoming JNLP agent as {@link Slave}. * * @author Kohsuke Kawaguchi * @since 1.561 * @since 1.614 handle() returns true on handshake error as it required in JnlpAgentReceiver. */ @Extension public class DefaultJnlpSlaveReceiver extends JnlpAgentReceiver { @Override public boolean handle(String nodeName, JnlpServerHandshake handshake) throws IOException, InterruptedException { SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName); if (computer==null) { return false; } Channel ch = computer.getChannel(); if (ch !=null) { String c = handshake.getRequestProperty("Cookie"); if (c!=null && c.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"); try { computer.disconnect(new ConnectionFromCurrentPeer()).get(15, TimeUnit.SECONDS); } catch (ExecutionException | TimeoutException e) { throw new IOException("Failed to disconnect the current client",e); } } else { handshake.error(nodeName + " is already connected to this master. Rejecting this connection."); return true; } } if (!matchesSecret(nodeName,handshake)) { handshake.error(nodeName + " can't be connected since the agent's secret does not match the handshake secret."); return true; } Properties response = new Properties(); String cookie = generateCookie(); response.put("Cookie",cookie); handshake.success(response); // this cast is leaking abstraction JnlpSlaveAgentProtocol2.Handler handler = (JnlpSlaveAgentProtocol2.Handler)handshake; ch = handler.jnlpConnect(computer); ch.setProperty(COOKIE_NAME, cookie); 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; } } private String generateCookie() { byte[] cookie = new byte[32]; new SecureRandom().nextBytes(cookie); return Util.toHexString(cookie); } private static final Logger LOGGER = Logger.getLogger(DefaultJnlpSlaveReceiver.class.getName()); private static final String COOKIE_NAME = JnlpSlaveAgentProtocol2.class.getName()+".cookie"; }