Skip to content

Commit

Permalink
[JENKINS-26580] Initial implementation of JNLP3-connect protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
akshayabd committed Apr 17, 2015
1 parent ba844a6 commit e044831
Show file tree
Hide file tree
Showing 17 changed files with 815 additions and 86 deletions.
3 changes: 2 additions & 1 deletion src/main/java/hudson/remoting/ChannelBuilder.java
Expand Up @@ -381,8 +381,9 @@ protected CommandTransport negotiate(final InputStream is, final OutputStream os
protected CommandTransport makeTransport(InputStream is, OutputStream os, Mode mode, Capability cap) throws IOException {
FlightRecorderInputStream fis = new FlightRecorderInputStream(is);

if (cap.supportsChunking())
if (cap.supportsChunking()) {
return new ChunkedCommandTransport(cap, mode.wrap(fis), mode.wrap(os), os);
}
else {
ObjectOutputStream oos = new ObjectOutputStream(mode.wrap(os));
oos.flush(); // make sure that stream preamble is sent to the other end. avoids dead-lock
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/hudson/remoting/ChunkedOutputStream.java
Expand Up @@ -94,4 +94,4 @@ private void sendFrame(boolean hasMore) throws IOException {
base.write(buf,0,size);
size = 0;
}
}
}
39 changes: 14 additions & 25 deletions src/main/java/hudson/remoting/Engine.java
Expand Up @@ -24,25 +24,18 @@
package hudson.remoting;

import hudson.remoting.Channel.Mode;
import org.jenkinsci.remoting.engine.EngineUtil;
import org.jenkinsci.remoting.engine.JnlpProtocol;
import org.jenkinsci.remoting.engine.JnlpProtocolFactory;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.net.MalformedURLException;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
Expand Down Expand Up @@ -163,7 +156,7 @@ public void removeListener(EngineListener el) {
@Override
public void run() {
// Create the protocols that will be attempted to connect to the master.
List<JnlpProtocol> protocols = JnlpProtocolFactory.createProtocols(secretKey, slaveName);
List<JnlpProtocol> protocols = JnlpProtocolFactory.createProtocols(slaveName, secretKey, events);

try {
boolean first = true;
Expand Down Expand Up @@ -239,40 +232,36 @@ public void run() {

events.status("Handshaking");
Socket jnlpSocket = connect(host,port);
DataOutputStream outputStream = new DataOutputStream(jnlpSocket.getOutputStream());
BufferedInputStream inputStream = new BufferedInputStream(jnlpSocket.getInputStream());
boolean connected = false;
ChannelBuilder channelBuilder = new ChannelBuilder("channel", executor)
.withJarCache(jarCache)
.withMode(Mode.BINARY);
Channel channel = null;

// Try available protocols.
for (JnlpProtocol protocol : protocols) {
events.status("Trying protocol: " + protocol.getName());
String response = protocol.performHandshake(outputStream, inputStream);
try {
channel = protocol.establishChannel(jnlpSocket, channelBuilder);
} catch (IOException ioe) {
events.status("Protocol failed to establish channel", ioe);
}

// On success do not try other protocols.
if (response.equals(GREETING_SUCCESS)) {
connected = true;
if (channel != null) {
break;
}

// On failure log the response and form a new connection.
events.status("Server didn't understand the protocol: " + response);
// On failure form a new connection.
jnlpSocket.close();
jnlpSocket = connect(host,port);
outputStream = new DataOutputStream(jnlpSocket.getOutputStream());
inputStream = new BufferedInputStream(jnlpSocket.getInputStream());
}

// If no protocol worked.
if (!connected) {
if (channel == null) {
onConnectionRejected("None of the protocols were accepted");
continue;
}

final Channel channel = new ChannelBuilder("channel", executor)
.withJarCache(jarCache)
.withMode(Mode.BINARY)
.build(inputStream, new BufferedOutputStream(jnlpSocket.getOutputStream()));

events.status("Connected");
channel.join();
events.status("Terminated");
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/org/jenkinsci/remoting/engine/EngineUtil.java
Expand Up @@ -57,6 +57,23 @@ public static String readLine(InputStream inputStream) throws IOException {
}
}

/**
* Read a certain amount of characters from the stream.
*
* @param inputStream The input stream to read from.
* @param len The amount of characters to read.
* @return The characters read.
* @throws IOException
*/
public static String readChars(InputStream inputStream, int len) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
for (int i = 0; i < len; i++) {
byteArrayOutputStream.write(inputStream.read());
}

return byteArrayOutputStream.toString("UTF-8");
}

/**
* Read headers from a response.
*
Expand Down
46 changes: 41 additions & 5 deletions src/main/java/org/jenkinsci/remoting/engine/JnlpProtocol.java
Expand Up @@ -23,9 +23,15 @@
*/
package org.jenkinsci.remoting.engine;

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

import javax.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

/**
* Handshake protocol used by JNLP slave when initiating a connection to
Expand All @@ -35,30 +41,60 @@
*/
public abstract class JnlpProtocol {

protected final String secretKey;
protected final String slaveName;
protected final String slaveSecret;
protected final EngineListenerSplitter events;

protected JnlpProtocol(String secretKey, String slaveName) {
this.secretKey = secretKey;
JnlpProtocol(String slaveName, String slaveSecret, EngineListenerSplitter events) {
this.slaveName = slaveName;
this.slaveSecret = slaveSecret;
this.events = events;
}

/**
* Get the name of the protocol.
*/
public abstract String getName();

/**
* Performs a handshake with the master and establishes a {@link Channel}.
*
* @param socket The established {@link Socket} connection to the master.
* @param channelBuilder The {@link ChannelBuilder} to use.
* @return The established channel if successful, else null.
* @throws IOException
*/
@Nullable
public Channel establishChannel(Socket socket, ChannelBuilder channelBuilder) throws IOException {
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());
BufferedInputStream inputStream = new BufferedInputStream(socket.getInputStream());
if(performHandshake(outputStream, inputStream)) {
return buildChannel(socket, channelBuilder);
}

return null;
}

/**
* Performs a handshake with the master.
*
* @param outputStream The stream to write into to initiate the handshake.
* @param inputStream The stream to read responses from the master.
* @return The greeting response from the master.
* @return true iff handshake was successful.
* @throws IOException
*/
public abstract String performHandshake(DataOutputStream outputStream,
abstract boolean performHandshake(DataOutputStream outputStream,
BufferedInputStream inputStream) throws IOException;

/**
* Builds a {@link Channel} which will be used for communication.
*
* @param socket The established {@link Socket} connection to the master.
* @param channelBuilder The {@link ChannelBuilder} to use.
* @return The constructed channel
*/
abstract Channel buildChannel(Socket socket, ChannelBuilder channelBuilder) throws IOException;

// The expected response from the master on successful completion of the
// handshake.
public static final String GREETING_SUCCESS = "Welcome";
Expand Down
31 changes: 25 additions & 6 deletions src/main/java/org/jenkinsci/remoting/engine/JnlpProtocol1.java
Expand Up @@ -23,9 +23,15 @@
*/
package org.jenkinsci.remoting.engine;

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

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

/**
* Implementation of the JNLP-connect protocol.
Expand All @@ -41,8 +47,8 @@
*/
class JnlpProtocol1 extends JnlpProtocol {

JnlpProtocol1(String secretKey, String slaveName) {
super(secretKey, slaveName);
JnlpProtocol1(String slaveName, String slaveSecret, EngineListenerSplitter events) {
super(slaveName, slaveSecret, events);
}

@Override
Expand All @@ -51,15 +57,28 @@ public String getName() {
}

@Override
public String performHandshake(DataOutputStream outputStream,
boolean performHandshake(DataOutputStream outputStream,
BufferedInputStream inputStream) throws IOException {
// Initiate the handshake.
outputStream.writeUTF(PROTOCOL_PREFIX + NAME);
outputStream.writeUTF(secretKey);
outputStream.writeUTF(slaveSecret);
outputStream.writeUTF(slaveName);

// Get the response from the master,
return EngineUtil.readLine(inputStream);
// Check if the server accepted.
String response = EngineUtil.readLine(inputStream);
if (!response.equals(GREETING_SUCCESS)) {
events.status("Server didn't accept the handshake: " + response);
return false;
}

return true;
}

@Override
Channel buildChannel(Socket socket, ChannelBuilder channelBuilder) throws IOException {
return channelBuilder.build(
new BufferedInputStream(socket.getInputStream()),
new BufferedOutputStream(socket.getOutputStream()));
}

static final String NAME = "JNLP-connect";
Expand Down
35 changes: 24 additions & 11 deletions src/main/java/org/jenkinsci/remoting/engine/JnlpProtocol2.java
Expand Up @@ -23,10 +23,16 @@
*/
package org.jenkinsci.remoting.engine;

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

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Properties;

/**
Expand All @@ -52,8 +58,8 @@ class JnlpProtocol2 extends JnlpProtocol {
*/
private String cookie;

JnlpProtocol2(String secretKey, String slaveName) {
super(secretKey, slaveName);
JnlpProtocol2(String slaveName, String slaveSecret, EngineListenerSplitter events) {
super(slaveName, slaveSecret, events);
}

@Override
Expand All @@ -66,25 +72,32 @@ String getCookie() {
}

@Override
public String performHandshake(DataOutputStream outputStream,
boolean performHandshake(DataOutputStream outputStream,
BufferedInputStream inputStream) throws IOException {
initiateHandshake(outputStream);

// Get the response from the master,
// Check if the server accepted.
String response = EngineUtil.readLine(inputStream);

// If success, look for the cookie.
if (response.equals(GREETING_SUCCESS)) {
Properties responses = EngineUtil.readResponseHeaders(inputStream);
cookie = responses.getProperty(COOKIE_KEY);
if (!response.equals(GREETING_SUCCESS)) {
events.status("Server didn't accept the handshake: " + response);
return false;
}

return response;
Properties responses = EngineUtil.readResponseHeaders(inputStream);
cookie = responses.getProperty(COOKIE_KEY);
return true;
}

@Override
Channel buildChannel(Socket socket, ChannelBuilder channelBuilder) throws IOException {
return channelBuilder.build(
new BufferedInputStream(socket.getInputStream()),
new BufferedOutputStream(socket.getOutputStream()));
}

private void initiateHandshake(DataOutputStream outputStream) throws IOException {
Properties props = new Properties();
props.put(SECRET_KEY, secretKey);
props.put(SECRET_KEY, slaveSecret);
props.put(SLAVE_NAME_KEY, slaveName);

// If there is a cookie send that as well.
Expand Down

0 comments on commit e044831

Please sign in to comment.