-
-
Notifications
You must be signed in to change notification settings - Fork 272
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #69 from jenkinsci/jnlp3
Activate JNLP3 protocol
- Loading branch information
Showing
6 changed files
with
313 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
src/main/java/org/jenkinsci/remoting/engine/JnlpServer3Handshake.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
132
src/main/java/org/jenkinsci/remoting/engine/JnlpServerHandshake.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} |
Oops, something went wrong.