Skip to content

Commit

Permalink
Merge pull request #69 from jenkinsci/jnlp3
Browse files Browse the repository at this point in the history
Activate JNLP3 protocol
  • Loading branch information
kohsuke committed Feb 3, 2016
2 parents 1e68eb8 + 80384c1 commit 54cec7a
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 15 deletions.
18 changes: 8 additions & 10 deletions src/main/java/org/jenkinsci/remoting/engine/HandshakeCiphers.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import java.security.spec.KeySpec;

/**
* {@link javax.crypto.Cipher}s that will be used to during the handshake
* {@link Cipher}s that will be used to during the handshake
* process for JNLP3 protocol.
*
* @author Akshay Dayal
Expand Down Expand Up @@ -90,32 +90,30 @@ public String decrypt(String encrypted) throws IOException {
}

/**
* Create a pair of AES symmetric key {@link javax.crypto.Cipher}s that
* Create a pair of AES symmetric key {@link Cipher}s that
* will be used during the handshake process.
*
* <p>The slave name and slave secret are used to create a
* {@link PBEKeySpec} and an {@link IvParameterSpec}which is then used to
* create the ciphers.
*
* @param slaveName The slave for which the handshake is taking place.
* @param slaveSecret The slave secret.
* @throws IOException If there is a problem creating the ciphers.
* @param salt The slave for which the handshake is taking place.
* @param secret The slave secret.
*/
public static HandshakeCiphers create(
String slaveName, String slaveSecret) throws IOException {
public static HandshakeCiphers create(String salt, String secret) {
try {
byte[] specKey = Jnlp3Util.generate128BitKey(slaveName + slaveSecret);
byte[] specKey = Jnlp3Util.generate128BitKey(salt + secret);
IvParameterSpec spec = new IvParameterSpec(specKey);

SecretKey secretKey = generateSecretKey(slaveName, slaveSecret);
SecretKey secretKey = generateSecretKey(salt, secret);
Cipher encryptCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
Cipher decryptCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, spec);

return new HandshakeCiphers(secretKey, spec, encryptCipher, decryptCipher);
} catch (GeneralSecurityException e) {
throw new IOException("Failed to create handshake ciphers", e);
throw (AssertionError)new AssertionError("Failed to create handshake ciphers").initCause(e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import hudson.remoting.Channel;
import hudson.remoting.ChannelBuilder;
import hudson.remoting.EngineListenerSplitter;
import hudson.remoting.EngineListener;

import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
Expand All @@ -37,7 +37,7 @@
import java.util.Properties;

import static org.jenkinsci.remoting.engine.EngineUtil.*;
import static org.jenkinsci.remoting.engine.Jnlp3Util.createChallengeResponse;
import static org.jenkinsci.remoting.engine.Jnlp3Util.*;

/**
* Implementation of the JNLP3-connect protocol.
Expand Down Expand Up @@ -103,6 +103,7 @@
* 256bit sizes are supported.
*
* @author Akshay Dayal
* @see JnlpServer3Handshake
*/
class JnlpProtocol3 extends JnlpProtocol {

Expand All @@ -114,7 +115,7 @@ class JnlpProtocol3 extends JnlpProtocol {
private String cookie;
private ChannelCiphers channelCiphers;

JnlpProtocol3(String slaveName, String slaveSecret, EngineListenerSplitter events) {
JnlpProtocol3(String slaveName, String slaveSecret, EngineListener events) {
super(slaveName, slaveSecret, events);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class JnlpProtocolFactory {
*/
public static List<JnlpProtocol> createProtocols(String slaveSecret, String slaveName, EngineListener events) {
return Arrays.asList(
new JnlpProtocol3(slaveName, slaveSecret, events),
new JnlpProtocol2(slaveName, slaveSecret, events),
new JnlpProtocol1(slaveName, slaveSecret, events)
);
Expand Down
166 changes: 166 additions & 0 deletions src/main/java/org/jenkinsci/remoting/engine/JnlpServer3Handshake.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package org.jenkinsci.remoting.engine;

import hudson.remoting.Channel;
import hudson.remoting.SocketChannelStream;
import org.jenkinsci.remoting.nio.NioChannelHub;

import javax.annotation.Nonnull;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.Random;
import java.util.concurrent.ExecutorService;

/**
* Server-side handshaking logic for {@link JnlpProtocol3}.
*
* @author Akshay Dayal
*/
public abstract class JnlpServer3Handshake extends JnlpServerHandshake {
/**
* If the client sends a connection cookie, that value is stored here.
*/
protected String cookie;

private HandshakeCiphers handshakeCiphers;

private String nodeName;

public JnlpServer3Handshake(NioChannelHub hub, ExecutorService threadPool, Socket socket) throws IOException {
super(hub,threadPool,socket);
}

/**
* Gets the node name reported from the other side.
*/
public String getNodeName() {
return nodeName;
}

/**
* Performs the handshake and establishes a channel.
*/
public Channel connect() throws IOException, InterruptedException {
try {
// Get initiation information from slave.
request.load(new ByteArrayInputStream(in.readUTF().getBytes(Charset.forName("UTF-8"))));
nodeName = request.getProperty(JnlpProtocol3.SLAVE_NAME_KEY);

this.handshakeCiphers = HandshakeCiphers.create(nodeName, getNodeSecret(nodeName));

authenticateToSlave();

// If there is a cookie decrypt it.
if (getRequestProperty(JnlpProtocol3.COOKIE_KEY) != null) {
cookie = handshakeCiphers.decrypt(getRequestProperty(JnlpProtocol3.COOKIE_KEY));
}

validateSlave();
} catch (Failure f) {
error(f.getMessage());
return null;
}

// Send greeting and new cookie.
out.println(JnlpProtocol.GREETING_SUCCESS);
String newCookie = generateCookie();
out.println(handshakeCiphers.encrypt(newCookie));

// Now get the channel cipher information.
String aesKeyString = handshakeCiphers.decrypt(in.readUTF());
String specKeyString = handshakeCiphers.decrypt(in.readUTF());
ChannelCiphers channelCiphers = ChannelCiphers.create(
Jnlp3Util.keyFromString(aesKeyString),
Jnlp3Util.keyFromString(specKeyString));

Channel channel = createChannelBuilder(nodeName).build(
new CipherInputStream(SocketChannelStream.in(socket),
channelCiphers.getDecryptCipher()),
new CipherOutputStream(SocketChannelStream.out(socket),
channelCiphers.getEncryptCipher()));

channel.setProperty(COOKIE_NAME, newCookie);

return channel;
}

/**
* Given the node name declared by the client, determine the secret
* used for authentication.
*
* @throws Failure
* if a fatal problem is found in determining the secret, to abort
* the handshake gracefully.
*/
protected abstract String getNodeSecret(String nodeName) throws Failure;

private void authenticateToSlave() throws IOException, Failure {
String challenge = handshakeCiphers.decrypt(
request.getProperty(JnlpProtocol3.CHALLENGE_KEY));

// Send slave challenge response.
String challengeResponse = Jnlp3Util.createChallengeResponse(challenge);
String encryptedChallengeResponse = handshakeCiphers.encrypt(challengeResponse);
out.println(encryptedChallengeResponse.getBytes(Charset.forName("UTF-8")).length);
out.print(encryptedChallengeResponse);
out.flush();

// If the slave accepted our challenge response send our challenge.
String challengeVerificationMessage = in.readUTF();
if (!challengeVerificationMessage.equals(JnlpProtocol.GREETING_SUCCESS)) {
throw new Failure("Slave did not accept our challenge response");
}
}

protected void validateSlave() throws IOException, Failure {
String masterChallenge = Jnlp3Util.generateChallenge();
String encryptedMasterChallenge = handshakeCiphers.encrypt(masterChallenge);
out.println(encryptedMasterChallenge.getBytes(Charset.forName("UTF-8")).length);
out.print(encryptedMasterChallenge);
out.flush();

// Verify the challenge response from the slave.
String encryptedMasterChallengeResponse = in.readUTF();
String masterChallengeResponse = handshakeCiphers.decrypt(
encryptedMasterChallengeResponse);
if (!Jnlp3Util.validateChallengeResponse(masterChallenge, masterChallengeResponse)) {
throw new Failure("Incorrect master challenge response from slave");
}
}

private String generateCookie() {
byte[] cookie = new byte[32];
RANDOM.nextBytes(cookie);
return toHexString(cookie);
}

@Nonnull
private String toHexString(@Nonnull byte[] bytes) {
StringBuilder buf = new StringBuilder();
for (byte bb : bytes) {
int b = bb & 0xFF;
if (b < 16) buf.append('0');
buf.append(Integer.toHexString(b));
}
return buf.toString();
}

/**
* Indicates a graceful handshake failure.
*
* This exception can be thrown during the handshake to refuse the inbound client.
*/
protected class Failure extends Exception {
public Failure(String msg) {
super(msg);
}
}

static final String COOKIE_NAME = JnlpProtocol3.class.getName() + ".cookie";

private static final Random RANDOM = new SecureRandom();
}
132 changes: 132 additions & 0 deletions src/main/java/org/jenkinsci/remoting/engine/JnlpServerHandshake.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package org.jenkinsci.remoting.engine;

import hudson.remoting.Channel;
import hudson.remoting.ChannelBuilder;
import org.jenkinsci.remoting.nio.NioChannelHub;

import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
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 JnlpServerHandshake {
/**
* Useful for creating a {@link Channel} with NIO as the underlying transport.
*/
protected final NioChannelHub hub;

/**
* Socket connection to the slave.
*/
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;

/**
* Bag of properties the JNLP agent have sent us during the hand-shake.
*/
protected final Properties request = new Properties();

private final ExecutorService threadPool;

protected JnlpServerHandshake(NioChannelHub hub, ExecutorService threadPool, Socket socket) throws IOException {
this.hub = hub;
this.threadPool = threadPool;
this.socket = socket;
this.in = new DataInputStream(socket.getInputStream());
this.out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")),true);
}

/**
* Copy constructor
*/
protected JnlpServerHandshake(JnlpServerHandshake rhs) {
this.hub = rhs.hub;
this.threadPool = rhs.threadPool;
this.socket = rhs.socket;
this.in = rhs.in;
this.out = rhs.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();
}

/**
* 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(JnlpProtocol.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, threadPool);
else
return hub.newChannelBuilder(nodeName, threadPool);
}


static final Logger LOGGER = Logger.getLogger(JnlpServerHandshake.class.getName());
}
Loading

0 comments on commit 54cec7a

Please sign in to comment.