提交 2524af13 编写于 作者: K Kohsuke Kawaguchi

Added a new extension point for more pluggable JNLP slave handling

上级 a7eb8b7e
......@@ -71,6 +71,8 @@ Upcoming changes</a>
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-22583">issue 22583</a>)
<li class=rfe>
Update version of bundled Mailer plugin to 1.8 to avoid issues with older versions
<li class=rfe>
Added a new extension point for more pluggable JNLP slave handling
</ul>
</div><!--=TRUNK-END=-->
......
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 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.Logger;
/**
* Match the name against the slave name and route the incoming JNLP agent as {@link Slave}.
*
* @author Kohsuke Kawaguchi
* @since 1.561
*/
@Extension
public class DefaultJnlpSlaveReceiver extends JnlpAgentReceiver {
@Override
public boolean handle(String nodeName, JnlpSlaveHandshake 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 e) {
throw new IOException("Failed to disconnect the current client",e);
} catch (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 false;
}
}
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;
}
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";
}
package jenkins.slaves;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Slave;
import jenkins.model.Jenkins;
import java.io.IOException;
import java.util.Properties;
/**
* Receives incoming slaves connecting through {@link JnlpSlaveAgentProtocol2}.
*
* <p>
* This is useful to establish the communication with other JVMs and use them
* for different purposes outside {@link Slave}s.
*
* @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, JnlpSlaveHandshake handshake) throws IOException, InterruptedException;
public static ExtensionList<JnlpAgentReceiver> all() {
return Jenkins.getInstance().getExtensionList(JnlpAgentReceiver.class);
}
}
......@@ -2,7 +2,6 @@ package jenkins.slaves;
import hudson.AbortException;
import hudson.Extension;
import hudson.model.Computer;
import hudson.remoting.Channel;
import hudson.remoting.Channel.Listener;
import hudson.remoting.ChannelBuilder;
......@@ -16,7 +15,6 @@ import org.jenkinsci.remoting.nio.NioChannelHub;
import javax.inject.Inject;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
......@@ -68,23 +66,7 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol {
new Handler(hub.getHub(),socket).run();
}
protected static class Handler {
protected final NioChannelHub hub;
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;
protected static class Handler extends JnlpSlaveHandshake {
/**
* @deprecated as of 1.559
......@@ -95,10 +77,9 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol {
}
public Handler(NioChannelHub hub, Socket socket) throws IOException {
this.hub = hub;
this.socket = socket;
in = new DataInputStream(socket.getInputStream());
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")),true);
super(hub,socket,
new DataInputStream(socket.getInputStream()),
new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")),true));
}
protected void run() throws IOException, InterruptedException {
......@@ -106,19 +87,19 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol {
final String nodeName = in.readUTF();
if(!SLAVE_SECRET.mac(nodeName).equals(secret)) {
error(out, "Unauthorized access");
error("Unauthorized access");
return;
}
SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName);
if(computer==null) {
error(out, "No such slave: "+nodeName);
error("No such slave: "+nodeName);
return;
}
if(computer.getChannel()!=null) {
error(out, nodeName+" is already connected to this master. Rejecting this connection.");
error(nodeName+" is already connected to this master. Rejecting this connection.");
return;
}
......@@ -134,12 +115,7 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol {
logw.println("JNLP agent connected from "+ socket.getInetAddress());
try {
ChannelBuilder cb;
if (hub==null)
cb = new ChannelBuilder(nodeName, Computer.threadPoolForRemoting);
else
cb = hub.newChannelBuilder(nodeName, Computer.threadPoolForRemoting);
ChannelBuilder cb = createChannelBuilder(nodeName);
computer.setChannel(cb.withHeaderStream(log).build(socket), log,
new Listener() {
......@@ -165,12 +141,6 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol {
throw e;
}
}
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());
......
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 jenkins.model.Jenkins;
import org.jenkinsci.remoting.nio.NioChannelHub;
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.
......@@ -62,69 +49,16 @@ public class JnlpSlaveAgentProtocol2 extends JnlpSlaveAgentProtocol {
*/
@Override
protected void run() throws IOException, InterruptedException {
Properties request = new Properties();
request.load(new ByteArrayInputStream(in.readUTF().getBytes("UTF-8")));
final String nodeName = request.getProperty("Node-Name");
if(!SLAVE_SECRET.mac(nodeName).equals(request.getProperty("Secret-Key"))) {
error(out, "Unauthorized access");
return;
}
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 IOException("Failed to disconnect the current client",e);
} catch (TimeoutException e) {
throw new IOException("Failed to disconnect the current client",e);
}
} else {
error(out, nodeName + " is already connected to this master. Rejecting this connection.");
for (JnlpAgentReceiver recv : JnlpAgentReceiver.all()) {
if (recv.handle(nodeName,this))
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<Object, Object> 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);
error("Unrecognized name: "+nodeName);
}
}
private static final Logger LOGGER = Logger.getLogger(JnlpSlaveAgentProtocol2.class.getName());
private static final String COOKIE_NAME = JnlpSlaveAgentProtocol2.class.getName()+".cookie";
}
package jenkins.slaves;
import hudson.model.Computer;
import hudson.remoting.Channel;
import hudson.remoting.ChannelBuilder;
import hudson.remoting.Engine;
import org.jenkinsci.remoting.nio.NioChannelHub;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Palette of objects to talk to the incoming JNLP slave connection.
*
* @author Kohsuke Kawaguchi
* @since 1.561
*/
public class JnlpSlaveHandshake {
/**
* Useful for creating a {@link Channel} with NIO as the underlying transport.
*/
/*package*/ final NioChannelHub hub;
/**
* Socket connection to the slave.
*/
/*package*/ final Socket socket;
/**
* Wrapping Socket input stream.
*/
/*package*/ 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.
*/
/*package*/ final PrintWriter out;
/**
* Bag of properties the JNLP agent have sent us during the hand-shake.
*/
/*package*/ final Properties request = new Properties();
/*package*/ JnlpSlaveHandshake(NioChannelHub hub, Socket socket, DataInputStream in, PrintWriter out) {
this.hub = hub;
this.socket = socket;
this.in = in;
this.out = out;
}
public NioChannelHub getHub() {
return hub;
}
public Socket getSocket() {
return socket;
}
public DataInputStream getIn() {
return in;
}
public PrintWriter getOut() {
return out;
}
public Properties getRequestProperties() {
return request;
}
public String getRequestProperty(String name) {
return request.getProperty(name);
}
/**
* Sends the error output and bail out.
*/
public void error(String msg) throws IOException {
out.println(msg);
LOGGER.log(Level.WARNING,Thread.currentThread().getName()+" is aborted: "+msg);
socket.close();
}
/**
* {@link JnlpAgentReceiver} calls this method to tell the client that the server
* is happy with the handshaking and is ready to move on to build a channel.
*/
public void success(Properties response) {
out.println(Engine.GREETING_SUCCESS);
for (Entry<Object, Object> e : response.entrySet()) {
out.println(e.getKey()+": "+e.getValue());
}
out.println(); // empty line to conclude the response header
}
public ChannelBuilder createChannelBuilder(String nodeName) {
if (hub==null)
return new ChannelBuilder(nodeName, Computer.threadPoolForRemoting);
else
return hub.newChannelBuilder(nodeName, Computer.threadPoolForRemoting);
}
private static final Logger LOGGER = Logger.getLogger(JnlpSlaveHandshake.class.getName());
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册