diff --git a/changelog.html b/changelog.html index 1e3d7df18c223de681d9b470223999ad4ea65e31..40e40345c8abfc328b7ab6143bba4b2fc0ae617b 100644 --- a/changelog.html +++ b/changelog.html @@ -83,6 +83,8 @@ Upcoming changes Added more context menus to hyperlinks in the console output
  • Exposed plugin manager and update center to the REST API +
  • + Added a new extension point for agent protocols.
  • Enabled concurrent build support for matrix projects (issue 6747) diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java index 2f2f35d41843fc4d2f093f2f2b5ea0dd477b5588..8483b0404086ab07e3a66fff13a7922095c5779f 100644 --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java @@ -23,81 +23,33 @@ */ package hudson; -import hudson.cli.Connection; -import hudson.util.Secret; -import jenkins.model.Jenkins; -import hudson.model.Computer; import hudson.slaves.OfflineCause; -import hudson.slaves.SlaveComputer; -import hudson.remoting.Channel; -import hudson.remoting.SocketOutputStream; -import hudson.remoting.SocketInputStream; -import hudson.remoting.Engine; -import hudson.remoting.Channel.Listener; -import hudson.remoting.Channel.Mode; -import hudson.cli.CliManagerImpl; -import hudson.cli.CliEntryPoint; -import hudson.util.IOException2; +import jenkins.AgentProtocol; -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import java.io.ByteArrayInputStream; import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.io.PrintWriter; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; -import java.net.BindException; -import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.Signature; -import java.util.Map.Entry; -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; /** * Listens to incoming TCP connections from JNLP slave agents and CLI. * - *

    Security

    - *

    - * Once connected, remote slave agents can send in commands to be - * executed on the master, so in a way this is like an rsh service. - * Therefore, it is important that we reject connections from - * unauthorized remote slaves. - * *

    - * The approach here is to have {@link jenkins.model.Jenkins#getSecretKey() a secret key} on the master. - * This key is sent to the slave inside the .jnlp file - * (this file itself is protected by HTTP form-based authentication that - * we use everywhere else in Hudson), and the slave sends this - * token back when it connects to the master. - * Unauthorized slaves can't access the protected .jnlp file, - * so it can't impersonate a valid slave. + * Aside from the HTTP endpoint, Jenkins runs {@link TcpSlaveAgentListener} that listens on a TCP socket. + * Historically this was used for inbound connection from slave agents (hence the name), but over time + * it was extended and made generic, so that multiple protocols of different purposes can co-exist on the + * same socket. * *

    - * We don't want to force the JNLP slave agents to be restarted - * whenever the server restarts, so right now this secret master key - * is generated once and used forever, which makes this whole scheme - * less secure. + * This class accepts the socket, then after a short handshaking, it dispatches to appropriate + * {@link AgentProtocol}s. * * @author Kohsuke Kawaguchi + * @see AgentProtocol */ public final class TcpSlaveAgentListener extends Thread { @@ -131,10 +83,6 @@ public final class TcpSlaveAgentListener extends Thread { return serverSocket.getLocalPort(); } - private String getSecretKey() { - return Jenkins.getInstance().getSecretKey(); - } - @Override public void run() { try { @@ -195,17 +143,11 @@ public final class TcpSlaveAgentListener extends Thread { if(s.startsWith("Protocol:")) { String protocol = s.substring(9); - if(protocol.equals("JNLP-connect")) { - runJnlpConnect(in, out); - } else if(protocol.equals("JNLP2-connect")) { - runJnlp2Connect(in, out); - } else if(protocol.equals("CLI-connect")) { - runCliConnect(in, out); - } else if(protocol.equals("CLI2-connect")) { - runCliConnect2(); - } else { + AgentProtocol p = AgentProtocol.of(protocol); + if (p!=null) + p.handle(this.s); + else error(out, "Unknown protocol:" + s); - } } else { error(out, "Unrecognized protocol: "+s); } @@ -226,185 +168,6 @@ public final class TcpSlaveAgentListener extends Thread { } } - /** - * Handles CLI connection request. - */ - private void runCliConnect(DataInputStream in, PrintWriter out) throws IOException, InterruptedException { - out.println("Welcome"); - runCli(new Connection(s)); - } - - /** - * CLI connection version2 that does transport encryption. - */ - private void runCliConnect2() throws IOException, InterruptedException { - try { - DataOutputStream out = new DataOutputStream(s.getOutputStream()); - out.writeUTF("Welcome"); - - // perform coin-toss and come up with a session key to encrypt data - Connection c = new Connection(s); - byte[] secret = c.diffieHellman(true).generateSecret(); - SecretKey sessionKey = new SecretKeySpec(Connection.fold(secret,128/8),"AES"); - c = c.encryptConnection(sessionKey,"AES/CFB8/NoPadding"); - - try { - // HACK: TODO: move the transport support into modules - Class cls = Jenkins.getInstance().pluginManager.uberClassLoader.loadClass("org.jenkinsci.main.modules.instance_identity.InstanceIdentity"); - Object iid = cls.getDeclaredMethod("get").invoke(null); - PrivateKey instanceId = (PrivateKey)cls.getDeclaredMethod("getPrivate").invoke(iid); - - // send a signature to prove our identity - Signature signer = Signature.getInstance("SHA1withRSA"); - signer.initSign(instanceId); - signer.update(secret); - c.writeByteArray(signer.sign()); - } catch (ClassNotFoundException e) { - throw new Error(e); - } catch (IllegalAccessException e) { - throw new Error(e); - } catch (InvocationTargetException e) { - throw new Error(e); - } catch (NoSuchMethodException e) { - throw new Error(e); - } - - runCli(c); - } catch (GeneralSecurityException e) { - throw new IOException2("Failed to encrypt the CLI channel",e); - } - } - - private void runCli(Connection c) throws IOException, InterruptedException { - Channel channel = new Channel("CLI channel from " + s.getInetAddress(), - Computer.threadPoolForRemoting, Mode.BINARY, - new BufferedInputStream(c.in), new BufferedOutputStream(c.out), null, true, Jenkins.getInstance().pluginManager.uberClassLoader); - channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel)); - channel.join(); - } - - /** - * Handles JNLP slave agent connection request. - */ - private void runJnlpConnect(DataInputStream in, PrintWriter out) throws IOException, InterruptedException { - if(!getSecretKey().equals(in.readUTF())) { - error(out, "Unauthorized access"); - return; - } - - final String nodeName = in.readUTF(); - SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName); - if(computer==null) { - error(out, "No such slave: "+nodeName); - return; - } - - if(computer.getChannel()!=null) { - error(out, nodeName+" is already connected to this master. Rejecting this connection."); - return; - } - - out.println(Engine.GREETING_SUCCESS); - - jnlpConnect(computer); - } - - /** - * Handles JNLP slave agent connection request (v2 protocol) - */ - private void runJnlp2Connect(DataInputStream in, PrintWriter out) throws IOException, InterruptedException { - Properties request = new Properties(); - request.load(new ByteArrayInputStream(in.readUTF().getBytes("UTF-8"))); - - if(!getSecretKey().equals(request.getProperty("Secret-Key"))) { - error(out, "Unauthorized access"); - return; - } - - final String nodeName = request.getProperty("Node-Name"); - SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName); - if(computer==null) { - error(out, "No such slave: "+nodeName); - return; - } - - Channel ch = computer.getChannel(); - if(ch !=null) { - String c = request.getProperty("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 e) { - throw new IOException2("Failed to disconnect the current client",e); - } catch (TimeoutException e) { - throw new IOException2("Failed to disconnect the current client",e); - } - } else { - error(out, nodeName + " is already connected to this master. Rejecting this connection."); - return; - } - } - - out.println(Engine.GREETING_SUCCESS); - - Properties response = new Properties(); - String cookie = generateCookie(); - response.put("Cookie",cookie); - writeResponseHeaders(out, response); - - ch = jnlpConnect(computer); - - ch.setProperty(COOKIE_NAME, cookie); - } - - private void writeResponseHeaders(PrintWriter out, Properties response) { - for (Entry e : response.entrySet()) { - out.println(e.getKey()+": "+e.getValue()); - } - out.println(); // empty line to conclude the response header - } - - private String generateCookie() { - byte[] cookie = new byte[32]; - new SecureRandom().nextBytes(cookie); - return Util.toHexString(cookie); - } - - private Channel jnlpConnect(SlaveComputer computer) throws InterruptedException, IOException { - final String nodeName = computer.getName(); - final OutputStream log = computer.openLogFile(); - PrintWriter logw = new PrintWriter(log,true); - logw.println("JNLP agent connected from "+ this.s.getInetAddress()); - - try { - computer.setChannel(new BufferedInputStream(this.s.getInputStream()), new BufferedOutputStream(this.s.getOutputStream()), log, - new Listener() { - @Override - public void onClosed(Channel channel, IOException cause) { - if(cause!=null) - LOGGER.log(Level.WARNING, "Connection #"+id+" for + " + nodeName + " terminated",cause); - try { - ConnectionHandler.this.s.close(); - } catch (IOException e) { - // ignore - } - } - }); - return computer.getChannel(); - } catch (AbortException e) { - logw.println(e.getMessage()); - logw.println("Failed to establish the connection with the slave"); - throw e; - } catch (IOException e) { - logw.println("Failed to establish the connection with the slave " + nodeName); - e.printStackTrace(logw); - throw e; - } - } - private void error(PrintWriter out, String msg) throws IOException { out.println(msg); LOGGER.log(Level.WARNING,"Connection #"+id+" is aborted: "+msg); @@ -425,8 +188,6 @@ public final class TcpSlaveAgentListener extends Thread { private static final Logger LOGGER = Logger.getLogger(TcpSlaveAgentListener.class.getName()); - private static final String COOKIE_NAME = TcpSlaveAgentListener.class.getName()+".cookie"; - /** * Host name that we advertise the CLI client to connect to. * This is primarily for those who have reverse proxies in place such that the HTTP host name diff --git a/core/src/main/java/hudson/cli/CliProtocol.java b/core/src/main/java/hudson/cli/CliProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..0f1d5035cfe50b48e3a41c3d32ac34e0c96f618d --- /dev/null +++ b/core/src/main/java/hudson/cli/CliProtocol.java @@ -0,0 +1,55 @@ +package hudson.cli; + +import hudson.Extension; +import hudson.model.Computer; +import hudson.remoting.Channel; +import hudson.remoting.Channel.Mode; +import jenkins.AgentProtocol; +import jenkins.model.Jenkins; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; + +/** + * {@link AgentProtocol} that accepts connection from CLI clients. + * + * @author Kohsuke Kawaguchi + * @since 1.467 + */ +@Extension +public class CliProtocol extends AgentProtocol { + @Override + public String getName() { + return "CLI-connect"; + } + + @Override + public void handle(Socket socket) throws IOException, InterruptedException { + new Handler(socket).run(); + } + + protected static class Handler { + protected final Socket socket; + + public Handler(Socket socket) { + this.socket = socket; + } + + public void run() throws IOException, InterruptedException { + PrintWriter out = new PrintWriter(socket.getOutputStream(),true); + out.println("Welcome"); + runCli(new Connection(socket)); + } + + protected void runCli(Connection c) throws IOException, InterruptedException { + Channel channel = new Channel("CLI channel from " + socket.getInetAddress(), + Computer.threadPoolForRemoting, Mode.BINARY, + new BufferedInputStream(c.in), new BufferedOutputStream(c.out), null, true, Jenkins.getInstance().pluginManager.uberClassLoader); + channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel)); + channel.join(); + } + } +} diff --git a/core/src/main/java/hudson/cli/CliProtocol2.java b/core/src/main/java/hudson/cli/CliProtocol2.java new file mode 100644 index 0000000000000000000000000000000000000000..f1cea1add398c40724aa2ad3df51ec425bd752e1 --- /dev/null +++ b/core/src/main/java/hudson/cli/CliProtocol2.java @@ -0,0 +1,79 @@ +package hudson.cli; + +import hudson.Extension; +import hudson.util.IOException2; +import jenkins.model.Jenkins; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.Signature; + +/** + * {@link CliProtocol} Version 2, which adds transport encryption. + * + * @author Kohsuke Kawaguchi + * @since 1.467 + */ +@Extension +public class CliProtocol2 extends CliProtocol { + @Override + public String getName() { + return "CLI2-connect"; + } + + @Override + public void handle(Socket socket) throws IOException, InterruptedException { + new Handler2(socket).run(); + } + + protected static class Handler2 extends Handler { + public Handler2(Socket socket) { + super(socket); + } + + @Override + public void run() throws IOException, InterruptedException { + try { + DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + out.writeUTF("Welcome"); + + // perform coin-toss and come up with a session key to encrypt data + Connection c = new Connection(socket); + byte[] secret = c.diffieHellman(true).generateSecret(); + SecretKey sessionKey = new SecretKeySpec(Connection.fold(secret,128/8),"AES"); + c = c.encryptConnection(sessionKey,"AES/CFB8/NoPadding"); + + try { + // HACK: TODO: move the transport support into modules + Class cls = Jenkins.getInstance().pluginManager.uberClassLoader.loadClass("org.jenkinsci.main.modules.instance_identity.InstanceIdentity"); + Object iid = cls.getDeclaredMethod("get").invoke(null); + PrivateKey instanceId = (PrivateKey)cls.getDeclaredMethod("getPrivate").invoke(iid); + + // send a signature to prove our identity + Signature signer = Signature.getInstance("SHA1withRSA"); + signer.initSign(instanceId); + signer.update(secret); + c.writeByteArray(signer.sign()); + } catch (ClassNotFoundException e) { + throw new Error(e); + } catch (IllegalAccessException e) { + throw new Error(e); + } catch (InvocationTargetException e) { + throw new Error(e); + } catch (NoSuchMethodException e) { + throw new Error(e); + } + + runCli(c); + } catch (GeneralSecurityException e) { + throw new IOException2("Failed to encrypt the CLI channel",e); + } + } + } +} diff --git a/core/src/main/java/jenkins/AgentProtocol.java b/core/src/main/java/jenkins/AgentProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..f6524c6695492de925b24da74ac0a34ec3ba4b73 --- /dev/null +++ b/core/src/main/java/jenkins/AgentProtocol.java @@ -0,0 +1,52 @@ +package jenkins; + +import hudson.Extension; +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.TcpSlaveAgentListener; +import hudson.model.AperiodicWork; +import jenkins.model.Jenkins; + +import java.io.IOException; +import java.net.Socket; + +/** + * Pluggable Jenkins TCP agent protocol handler called from {@link TcpSlaveAgentListener}. + * + *

    + * To register your extension, put {@link Extension} annotation on your subtype. + * Implementations of this extension point is singleton, and its {@link #handle(Socket)} method + * gets invoked concurrently whenever a new connection comes in. + * + * @author Kohsuke Kawaguchi + * @since 1.467 + * @see TcpSlaveAgentListener + */ +public abstract class AgentProtocol implements ExtensionPoint { + /** + * Protocol name. + * + * This is a short string that consists of printable ASCII chars. Sent by the client to select the protocol. + */ + public abstract String getName(); + + /** + * Called by the connection handling thread to execute the protocol. + */ + public abstract void handle(Socket socket) throws IOException, InterruptedException; + + /** + * Returns all the registered {@link AperiodicWork}s. + */ + public static ExtensionList all() { + return Jenkins.getInstance().getExtensionList(AgentProtocol.class); + } + + public static AgentProtocol of(String protocolName) { + for (AgentProtocol p : all()) { + if (p.getName().equals(protocolName)) + return p; + } + return null; + } +} diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..2050ce1ecd2bc778128202cdb2c2bb576b518502 --- /dev/null +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java @@ -0,0 +1,153 @@ +package jenkins.slaves; + +import hudson.AbortException; +import hudson.Extension; +import hudson.remoting.Channel; +import hudson.remoting.Channel.Listener; +import hudson.remoting.Engine; +import hudson.slaves.SlaveComputer; +import jenkins.AgentProtocol; +import jenkins.model.Jenkins; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * {@link AgentProtocol} that accepts connection from slave agents. + * + *

    Security

    + *

    + * Once connected, remote slave agents can send in commands to be + * executed on the master, so in a way this is like an rsh service. + * Therefore, it is important that we reject connections from + * unauthorized remote slaves. + * + *

    + * The approach here is to have {@link Jenkins#getSecretKey() a secret key} on the master. + * This key is sent to the slave inside the .jnlp file + * (this file itself is protected by HTTP form-based authentication that + * we use everywhere else in Hudson), and the slave sends this + * token back when it connects to the master. + * Unauthorized slaves can't access the protected .jnlp file, + * so it can't impersonate a valid slave. + * + *

    + * We don't want to force the JNLP slave agents to be restarted + * whenever the server restarts, so right now this secret master key + * is generated once and used forever, which makes this whole scheme + * less secure. + * + * @author Kohsuke Kawaguchi + * @since 1.467 + */ +@Extension +public class JnlpSlaveAgentProtocol extends AgentProtocol { + @Override + public String getName() { + return "JNLP-connect"; + } + + @Override + public void handle(Socket socket) throws IOException, InterruptedException { + new Handler(socket).run(); + } + + protected static class Handler { + protected final Socket socket; + + /** + * Wrapping Socket input stream. + */ + protected final DataInputStream in; + + /** + * For writing handshaking response. + * + * This is a poor design choice that we just carry forward for compatibility. + * For better protocol design, {@link DataOutputStream} is preferred for newer + * protocols. + */ + protected final PrintWriter out; + + public Handler(Socket socket) throws IOException { + this.socket = socket; + in = new DataInputStream(socket.getInputStream()); + out = new PrintWriter(socket.getOutputStream(),true); + } + + protected void run() throws IOException, InterruptedException { + if(!getSecretKey().equals(in.readUTF())) { + error(out, "Unauthorized access"); + return; + } + + final String nodeName = in.readUTF(); + SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName); + if(computer==null) { + error(out, "No such slave: "+nodeName); + return; + } + + if(computer.getChannel()!=null) { + error(out, nodeName+" is already connected to this master. Rejecting this connection."); + return; + } + + out.println(Engine.GREETING_SUCCESS); + + jnlpConnect(computer); + } + + protected Channel jnlpConnect(SlaveComputer computer) throws InterruptedException, IOException { + final String nodeName = computer.getName(); + final OutputStream log = computer.openLogFile(); + PrintWriter logw = new PrintWriter(log,true); + logw.println("JNLP agent connected from "+ socket.getInetAddress()); + + try { + computer.setChannel(new BufferedInputStream(socket.getInputStream()), new BufferedOutputStream(socket.getOutputStream()), log, + new Listener() { + @Override + public void onClosed(Channel channel, IOException cause) { + if(cause!=null) + LOGGER.log(Level.WARNING, Thread.currentThread().getName()+" for + " + nodeName + " terminated",cause); + try { + socket.close(); + } catch (IOException e) { + // ignore + } + } + }); + return computer.getChannel(); + } catch (AbortException e) { + logw.println(e.getMessage()); + logw.println("Failed to establish the connection with the slave"); + throw e; + } catch (IOException e) { + logw.println("Failed to establish the connection with the slave " + nodeName); + e.printStackTrace(logw); + throw e; + } + } + + protected String getSecretKey() { + return Jenkins.getInstance().getSecretKey(); + } + + protected void error(PrintWriter out, String msg) throws IOException { + out.println(msg); + LOGGER.log(Level.WARNING,Thread.currentThread().getName()+" is aborted: "+msg); + socket.close(); + } + } + + private static final Logger LOGGER = Logger.getLogger(JnlpSlaveAgentProtocol.class.getName()); +} diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java new file mode 100644 index 0000000000000000000000000000000000000000..413486e38df17104b3dd48fc1693e4add8df0115 --- /dev/null +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java @@ -0,0 +1,121 @@ +package jenkins.slaves; + +import hudson.Extension; +import hudson.TcpSlaveAgentListener.ConnectionFromCurrentPeer; +import hudson.Util; +import hudson.remoting.Channel; +import hudson.remoting.Engine; +import hudson.slaves.SlaveComputer; +import hudson.util.IOException2; +import jenkins.model.Jenkins; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; +import java.security.SecureRandom; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; + +/** + * {@link JnlpSlaveAgentProtocol} Version 2. + * + *

    + * This protocol extends the version 1 protocol by adding a per-client cookie, + * so that we can detect a reconnection from the slave and take appropriate action, + * when the connection disappered without the master noticing. + * + * @author Kohsuke Kawaguchi + * @since 1.467 + */ +@Extension +public class JnlpSlaveAgentProtocol2 extends JnlpSlaveAgentProtocol { + @Override + public String getName() { + return "JNLP2-connect"; + } + + @Override + public void handle(Socket socket) throws IOException, InterruptedException { + new Handler2(socket).run(); + } + + protected static class Handler2 extends Handler { + public Handler2(Socket socket) throws IOException { + super(socket); + } + + /** + * Handles JNLP slave agent connection request (v2 protocol) + */ + @Override + protected void run() throws IOException, InterruptedException { + Properties request = new Properties(); + request.load(new ByteArrayInputStream(in.readUTF().getBytes("UTF-8"))); + + if(!getSecretKey().equals(request.getProperty("Secret-Key"))) { + error(out, "Unauthorized access"); + return; + } + + final String nodeName = request.getProperty("Node-Name"); + SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName); + if(computer==null) { + error(out, "No such slave: "+nodeName); + return; + } + + Channel ch = computer.getChannel(); + if(ch !=null) { + String c = request.getProperty("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 e) { + throw new IOException2("Failed to disconnect the current client",e); + } catch (TimeoutException e) { + throw new IOException2("Failed to disconnect the current client",e); + } + } else { + error(out, nodeName + " is already connected to this master. Rejecting this connection."); + return; + } + } + + out.println(Engine.GREETING_SUCCESS); + + Properties response = new Properties(); + String cookie = generateCookie(); + response.put("Cookie",cookie); + writeResponseHeaders(out, response); + + ch = jnlpConnect(computer); + + ch.setProperty(COOKIE_NAME, cookie); + } + + private void writeResponseHeaders(PrintWriter out, Properties response) { + for (Entry e : response.entrySet()) { + out.println(e.getKey()+": "+e.getValue()); + } + out.println(); // empty line to conclude the response header + } + + private String generateCookie() { + byte[] cookie = new byte[32]; + new SecureRandom().nextBytes(cookie); + return Util.toHexString(cookie); + } + } + + private static final Logger LOGGER = Logger.getLogger(JnlpSlaveAgentProtocol2.class.getName()); + + private static final String COOKIE_NAME = JnlpSlaveAgentProtocol2.class.getName()+".cookie"; +}