package jenkins.slaves;
import hudson.AbortException;
import hudson.Extension;
import hudson.Util;
import hudson.model.Computer;
import hudson.remoting.Channel;
import hudson.remoting.Channel.Listener;
import hudson.remoting.ChannelBuilder;
import hudson.remoting.Engine;
import hudson.slaves.SlaveComputer;
import jenkins.AgentProtocol;
import jenkins.model.Jenkins;
import jenkins.security.ChannelConfigurator;
import jenkins.security.HMACConfidentialKey;
import org.jenkinsci.Symbol;
import org.jenkinsci.remoting.engine.JnlpServerHandshake;
import org.jenkinsci.remoting.nio.NioChannelHub;
import javax.inject.Inject;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* {@link AgentProtocol} that accepts connection from agents.
*
* <h2>Security</h2>
* <p>
* Once connected, remote 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 agents.
*
* <p>
* We do this by computing HMAC of the agent name.
* This code is sent to the agent inside the <tt>.jnlp</tt> file
* (this file itself is protected by HTTP form-based authentication that
* we use everywhere else in Jenkins), and the agent sends this
* token back when it connects to the master.
* Unauthorized agents can't access the protected <tt>.jnlp</tt> file,
* so it can't impersonate a valid agent.
*
* <p>
* We don't want to force the JNLP 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 @Symbol("jnlp")
public class JnlpSlaveAgentProtocol extends AgentProtocol {
@Inject
NioChannelSelector hub;
/**
* {@inheritDoc}
*/
@Override
public boolean isOptIn() {
return OPT_IN;
}
@Override
public String getName() {
return "JNLP-connect";
}
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.JnlpSlaveAgentProtocol_displayName();
}
@Override
public void handle(Socket socket) throws IOException, InterruptedException {
new Handler(hub.getHub(),socket).run();
}
protected static class Handler extends JnlpServerHandshake {
/**
* @deprecated as of 1.559
* Use {@link #Handler(NioChannelHub, Socket)}
*/
@Deprecated
public Handler(Socket socket) throws IOException {
this(null,socket);
}
public Handler(NioChannelHub hub, Socket socket) throws IOException {
super(hub, Computer.threadPoolForRemoting, socket);
}
protected void run() throws IOException, InterruptedException {
final String secret = in.readUTF();
final String nodeName = in.readUTF();
if(!SLAVE_SECRET.mac(nodeName).equals(secret)) {
error("Unauthorized access");
return;
}
SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName);
if(computer==null) {
error("No such agent: "+nodeName);
return;
}
if(computer.getChannel()!=null) {
error(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 {
ChannelBuilder cb = createChannelBuilder(nodeName);
for (ChannelConfigurator cc : ChannelConfigurator.all()) {
cc.onChannelBuilding(cb, computer);
}
computer.setChannel(cb.withHeaderStream(log).build(socket), 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 agent");
throw e;
} catch (IOException e) {
logw.println("Failed to establish the connection with the agent " + nodeName);
e.printStackTrace(logw);
throw e;
}
}
}
private static final Logger LOGGER = Logger.getLogger(JnlpSlaveAgentProtocol.class.getName());
/**
* This secret value is used as a seed for agents.
*/
public static final HMACConfidentialKey SLAVE_SECRET = new HMACConfidentialKey(JnlpSlaveAgentProtocol.class,"secret");
/**
* A/B test turning off this protocol by default.
*/
private static final boolean OPT_IN;
static {
byte hash = Util.fromHexString(Jenkins.getInstance().getLegacyInstanceId())[0];
OPT_IN = (hash % 10) == 0;
}
}