Hello,
For a specific need, I've done a simple HTTP server which receives HTTPS incoming POST requests. The requests are a simple HTTP 1.1 request which post data using XML inside of the content.
My problem is that often (not always) my server is not able to extract the content of the request. I've tried both to read data from the socket channel using a loop or to wait for an OP_READ to occured on a selector (I use non blocking IO) but in the first case I loop indefinitely, in the second case I wait indefinitely. Using Ethereal I am sure that data are well sent from the client up to the server and when using no ssl connection it seems to work fine.
Does anybody have successfuly used SSLEngine and non blocking IO together?
Server : JDK 1.5.06 using SSLEngine and nio
Client : JDK 1.5.06 using standard IO with HTTPS protocol
The Server code for managing an OP_ACCEPT (done inside a specific thread because of SSL handshake which can be time consuming:
package net.atos.mm.fwk.fw3.push.connections.gateway;
import ...;
/**
* A task which has its own thread and is launched by the SelectorThread each time a new connection arrived.
* This task manage the connection:
* - SSL hanshake
* - ...
*/
public class SelectorTask implements Callable {
// The selector thread
SelectorThread selectorThread;
// The selection key
SelectionKey selectionKey;
// The channel
SocketChannel channel;
// Do we use SSL ?
boolean useSSL;
// SSL context
SSLContext sslContext;
// Single selector for reads
Selector readDataSelector = null;
// The Http request associated with this connection establishment
HttpRequest httpRequest = null;
/**
* Create a new selector task
* @param selectorThread
* @param selectionKey
* @param channel
* @param sslContext
* @throws IOException
*/
public SelectorTask(SelectorThread selectorThread, SelectionKey selectionKey, SocketChannel channel, SSLContext sslContext) throws IOException {
this.selectorThread = selectorThread;
this.selectionKey = selectionKey;
this.channel = channel;
this.sslContext = sslContext;
this.useSSL = sslContext!=null;
// Configure the channel in non blocking mode
channel.configureBlocking(false);
// Register for Reads inside our selector
readDataSelector = SelectorProvider.provider().openSelector();
channel.register(readDataSelector,SelectionKey.OP_READ);
}
/**
* This method is called by a new Thread
* It manages the connection (SSL handshake and so on...)
*/
public Object call() throws Exception {
// While we are not connected and there are data to read ...
while (!connected && readDataSelector.select() > 0){
Set readyKeys = readDataSelector.selectedKeys();
Iterator i = readyKeys.iterator();
while (i.hasNext()) {
SelectionKey selectionKey = (SelectionKey) i.next();
i.remove();
// Check the key for ead or write
if (selectionKey.isReadable() || selectionKey.isWritable()){
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
// Create a SocketConnection to handle the socket between the client and the gateway
// If none has been prevously created
if (selectionKey.attachment()==null){
// For SSL we must create a SSL engine
SSLEngine sslEngine = null;
if (useSSL){
// Create engine
sslEngine = sslContext.createSSLEngine();
sslEngine.setUseClientMode(false);
}
SocketConnection socketConnection = new SocketConnection(socketChannel, sslEngine);
selectionKey.attach(socketConnection);
}
// Call the method to handle send/receive data
try {
receiveOrSendData (socketChannel, selectionKey);
}
catch (IOException e){
logger.dumpException(e, "an exception occured while receiving a client connection");
return null;
}
catch (Exception e){
logger.dumpException(e, "an exception occured while receiving a client connection");
return null;
}
}
}
}
// Close the local selector
readDataSelector.close();
// Register this channel inside the main selector to get notified of the close
selectorThread.registerClientChannel(channel);
return null;
}
/**
* This method is called each time we receive data from the client or
* each time we can write data (handshake in SSL mode)
* @param selectionKey the selection key
* @throws IOException
*/
private void receiveOrSendData(SocketChannel socketChannel, SelectionKey selectionKey)throws IOException{
// This flag is used to check if a method has finished its job
boolean workFinished = true;
// Get the associated socket connection
SocketConnection socketConnection = (SocketConnection)selectionKey.attachment();
try {
// No client connection
// For SSL we have to handshake
// If SSL do handshake
if (useSSL){
workFinished = socketConnection.doHandshake(selectionKey);
}
// If the handshake is already finished or if no SSL is used,
// we can create an HttpRequest to get request data
if (workFinished){
// Create an HttpRequest object from the data received on the channel
if (httpRequest==null)
httpRequest = new HttpRequest(socketConnection);
// Read data to populate the Http Request object
workFinished = httpRequest.parseRequest(selectionKey);
if (workFinished){
if ((httpRequest.contentLength == 0)){
// If no data has been send close the channel
socketConnection.close();
}
else{
// Reply to request
// ....
}
}
}
else{
// In case of SSL the handshake is not finished, we must
// wait for I/O : sending data or new data to come.
return;
}
}catch(BufferUnderflowException ex){
// Not enougth data: need to read more data
}catch(IOException ex){
// If an exception occured it is probably because the client connection
// has been lost : we close everything
// ...
throw ex;
}
}
}
The HttpRequest object used to parse incoming data
package net.atos.mm.fwk.fw3.push.connections.gateway;
import ...;
/**
* An incoming Http request instance
*/
public class HttpRequest{
// Size of buffer used to read data
private static final int BUFFER_SIZE = 32768;
/**
* The user agent
*/
public String useragent;
/**
* The complete request : POST ...
*/
public String requestString;
/**
* The command send
*/
public String command;
/**
* The content length
*/
public int contentLength;
/**
* The content of the request
*/
public ByteBuffer content;
// The socket connection
SocketConnection connection;
// Do header have been all read ?
boolean headerRead = false;
boolean contentRead = false;
// The buffer used to read data
ByteBuffer buffer;
/**
* Create a new HttpRequest object to parse the incoming request
* @param connection the connection to the client
*/
public HttpRequest(SocketConnection connection) {
this.connection = connection;
// Allocate byte buffer
buffer = ByteBuffer.allocate(BUFFER_SIZE);
content = ByteBuffer.allocate(BUFFER_SIZE);
contentLength = 0;
buffer.clear();
}
/**
* Read Http headers and content from an incoming request
* @return true if we have finished, false else
* @throws IOException
*/
public boolean parseRequest(SelectionKey selectionKey) throws IOException {
boolean allDone = true;
// Read data
try {
connection.readData(buffer);
}
catch (BufferUnderflowException e){
// Need to read more data
return false;
}
// Read headers from the request
allDone = readHeaders(selectionKey);
if (!allDone){
buffer.clear();
return false;
}
// Read content
allDone = readContent(selectionKey);
if (!allDone){
buffer.clear();
return false;
}
return true;
}
/**
* Read a line from the request buffer
* @return the new line
*/
private String readLine (){
StringBuffer line = new StringBuffer();
boolean eol = false;
char current;
while (!eol){
if (buffer.remaining()==0)
// We are at the end of the buffer
eol = true;
else {
current = (char)buffer.get();
switch (current){
// Get a \r , test if there is an \n
case '\r':
current = (char)buffer.get();
if (current=='\n')
eol = true;
else {
line.append('\r').append(current);
}
break;
// Get a \n , test if there is an \r
case '\n':
current = (char)buffer.get();
if (current=='\r')
eol = true;
else {
line.append('\n').append(current);
}
break;
// Just a simple char, append it
default:
line.append(current);
}
}
}
return line.toString();
}
/**
* Read headers from request
* @param requestReader the reader used to read data from
* @return true if all headers have been read, false else
* @throws IOException
*/
private boolean readHeaders(SelectionKey selectionKey) throws IOException{
if (headerRead)
return true;
// Read all the header lines
String currentLine = readLine();
while (currentLine!=null && currentLine.length()>0){
if (currentLine.startsWith("POST")){
this.requestString = currentLine;
}
else {
StringTokenizer tok = new StringTokenizer(currentLine, ":");
if (tok.countTokens() >= 2) {
String keyname = tok.nextToken().trim();
if (keyname.toLowerCase().equals("user-agent")) {
useragent = tok.nextToken().trim();
} else if (keyname.toLowerCase().equals("content-length")) {
contentLength = Integer
.parseInt(tok.nextToken().trim());
}
}
}
currentLine = readLine();
}
// Compact the buffer
buffer.compact();
buffer.flip();
headerRead = true;
return true;
}
/**
* Read content from the request
* @param requestBytes the byte buffer read from the request
* @return true if all data have been read, false else
* @throws IOException
*/
private boolean readContent(SelectionKey selectionKey) throws IOException {
if (contentRead)
return true;
// Add all remaining data from the request (after headers)
content.put(buffer);
// If we have not enough data try to read more data from the connection
int nbRead = 1;
while (content.position()<contentLength){
try {
buffer.clear();
nbRead = connection.readData(buffer);
if (nbRead>0){
content.put(buffer);
}
}
catch (BufferUnderflowException e){
e.printStackTrace();
selectionKey.interestOps(SelectionKey.OP_READ);
return false;
}
try {Thread.sleep(2000);}catch (Exception e){}
}
// If we have read all the data flip buffer
if (content.position()>=contentLength){
contentRead = true;
content.flip();
return true;
}
selectionKey.interestOps(SelectionKey.OP_READ);
return false;
}
}
The SocketConnection class used to read/write data from/to the client and to handshake:
package net.atos.mm.fwk.fw3.push.connections.gateway;
import ...
/**
* Socket level connection to the client
*/
public class SocketConnection {
// The socket associated with this connection
SocketChannel socketChannel;
// The SSL Engine associated with this socket
SSLEngine sslEngine;
// The SSL Session
SSLSession sslSession;
// SSLEngine result when wrapping/unwrapping data
SSLEngineResult result;
// The current status
SSLEngineResult.Status status;
SSLEngineResult.HandshakeStatus hsStatus = HandshakeStatus.NEED_UNWRAP;
// Buffers
ByteBuffer inNetData; // Incoming encrypted data
ByteBuffer outNetData; // Outgoing encrypted data
ByteBuffer inAppData; // Incoming decrypted data
ByteBuffer outAppData; // Outgoing decrypted data
/*
* An empty ByteBuffer for use when one isn't available, say
* as a source buffer during initial handshake wraps or for close
* operations.
*/
static ByteBuffer hsBB = ByteBuffer.allocate(0);
// Is handshake completed ?
boolean handshakeCompleted = false;
/**
* Create a new Socket connection
* @param socketChannel the channel
* @param sslEngine the SSL engine
*/
public SocketConnection (SocketChannel socketChannel, SSLEngine sslEngine){
this.socketChannel = socketChannel;
this.sslEngine = sslEngine;
if (sslEngine!=null){
// Get SSL session
sslSession = sslEngine.getSession();
// Initialize buffers used for I/O
int size = sslSession.getPacketBufferSize();
inNetData = ByteBuffer.allocate(size);
outNetData = ByteBuffer.allocate(size);
outNetData.position(0).limit(0);
size = sslSession.getApplicationBufferSize();
inAppData = ByteBuffer.allocate(size);
outAppData = ByteBuffer.allocate(size);
}
}
/**
* Close the connection
* @throws IOException
*/
public void close() throws IOException{
socketChannel.close();
}
/**
* For an SSL connection do the handshake
* This method is called many times (for each I/O) until the handshake is completed
* @return true is the handshake has been completed, false if there are more things to do
*/
public boolean doHandshake (SelectionKey selectionKey) throws IOException{
/*<dbg>System.out.println("[SocketConnection - doHandShake]");</dbg>*/
// Test if the handshake has been completed
if (handshakeCompleted){
return true;
}
// Do handshake
// -----------
// We have to write data if there are any available data into the output net buffer
// This occured when our selection key for WRITE_OP is waken up after we have wrapped
// data during the previous call
if (outNetData.hasRemaining()){
// Writing data
writeRawData(outNetData);
// Test if the handshake is completed or if we need to switch from write to read mode
switch (hsStatus) {
// Handshake completed
case FINISHED:
handshakeCompleted = true;
// Handshake has been completed, now we can read the request data so we don't
// do a break here to switch to read mode
// Need to read data
case NEED_UNWRAP:
selectionKey.interestOps(SelectionKey.OP_READ);
break;
}
return handshakeCompleted;
}
// Depending on the handshake status we do something...
switch (hsStatus) {
// We need to read more data
case NEED_UNWRAP:
while (hsStatus == HandshakeStatus.NEED_UNWRAP) {
try {
// Reading data
readRawData(inNetData);
}
catch (IOException e){
// An exception occured, the socket has been closed
sslEngine.closeInbound();
throw e;
}
// Unwrapping data
// ---------------
unwrappHandshake (selectionKey);
}
// If we need to wrapp go to the next case, else we break
if (hsStatus != HandshakeStatus.NEED_WRAP) {
break;
}
// We need to wrapp data
case NEED_WRAP:
// Wrapping data
wrappHandshake(selectionKey);
break;
default: // NOT_HANDSHAKING/NEED_TASK/FINISHED
throw new RuntimeException("Invalid Handshaking State" +hsStatus);
}
return handshakeCompleted;
}
/**
* Unwrapping (decrypting) data during handshake
* @param selectionKey the selection key
* @throws IOException
*/
private void unwrappHandshake(SelectionKey selectionKey) throws IOException{
/*<dbg>System.out.println("[SocketConnection - unwrappHandshake]");</dbg>*/
// Flip the buffer : limit is set to current position and position is set to 0
inNetData.flip();
// Unwrapping data using the SSL engine
// The output buffer inAppData wille never be filled
result = sslEngine.unwrap(inNetData, inAppData);
// Compact the input buffer and update status
inNetData.compact();
status = result.getStatus();
hsStatus = result.getHandshakeStatus();
// Depending on the status codes we can:
// - throw an exception : not handshaking, connection closed, buffer overflow
// - do tasks
// - set handshakeCompleted to true
// - switch to read mode if we have to read more data
switch (status) {
case OK:
switch (hsStatus) {
case NOT_HANDSHAKING:
throw new IOException("Not handshaking during initial handshake");
case NEED_TASK:
/*<dbg>System.out.println("[SocketConnection - unwrappHandshake] need task");</dbg>*/
hsStatus = doTasks();
break;
case FINISHED:
System.out.println("[SocketConnection - unwrappHandshake] handshake finished");
handshakeCompleted = true;
selectionKey.interestOps(SelectionKey.OP_READ);
return;
}
break;
case BUFFER_UNDERFLOW:
// Need to read more data
if (selectionKey != null) {
selectionKey.interestOps(SelectionKey.OP_READ);
}
return;
default: // BUFFER_OVERFLOW/CLOSED:
throw new IOException("Received" + result.getStatus() +" during initial handshaking");
}
}
/**
* Wrapping (encrypting) data during handshake
* @param selectionKey the selection key
* @throws IOException if the connection has been closed or handshake not running correctly
*/
private void wrappHandshake (SelectionKey selectionKey) throws IOException{
/*<dbg>System.out.println("[SocketConnection - wrappHandshake]");</dbg>*/
// Clear the output buffer
outNetData.clear();
// Wrapping data: the input buffer hsBB is not used
result = sslEngine.wrap(hsBB, outNetData);
// Flip the buffer : limit is set to the current position, index is set to 0
outNetData.flip();
// Update status
status = result.getStatus();
hsStatus = result.getHandshakeStatus();
// Depending on the status code we can:
// - do tasks
// - throw an IOException : buffer overflow, buffer underflow or connection closed
// In case of OK status wea always have to switch to write mode in order to write to wrapped
// data to the socket
switch (status) {
case OK:
if (hsStatus == HandshakeStatus.NEED_TASK) {
hsStatus = doTasks();
}
// Switch to write mode in order to write the wrapped data
selectionKey.interestOps(SelectionKey.OP_WRITE);
break;
default: // BUFFER_OVERFLOW/BUFFER_UNDERFLOW/CLOSED:
throw new IOException("Received " + result.getStatus() +" during initial handshaking");
}
}
/**
* Read data from the socket, decrypting it if needed
* @param contentBuffer the buffer in which data will be read
* @return the number of bytes read
*/
public int readData (ByteBuffer contentBuffer) throws IOException {
int nbRead = 0;
// If no SSL directly read inside the final buffer
ByteBuffer readBuffer = contentBuffer;
// If SSL use the inNetData to read crypted data
if (sslEngine!=null){
readBuffer = inNetData;
}
// Read crypted data for SSL or uncrypted data else
nbRead = readRawData(readBuffer);
// If SSL decrypt data inside the final buffer
if (sslEngine!=null && nbRead>0){
readBuffer.flip();
result = sslEngine.unwrap(readBuffer, contentBuffer);
readBuffer.compact();
status = result.getStatus();
switch (status){
case BUFFER_OVERFLOW:
throw new IOException ("Buffer overflow [NetBuffer] position="+readBuffer.position()+" limit="+readBuffer.limit()+" nbRead="+nbRead+ "[ContentBuffer] position="+contentBuffer.position()+" limit="+contentBuffer.limit());
case BUFFER_UNDERFLOW:
// Have to read more data
throw new BufferUnderflowException();
default:
inNetData.clear();
}
}
// Flip the buffer read
contentBuffer.flip();
return nbRead;
}
/**
* Read raw data without decrypting them
* @param buffer the buffer to read data into
* @return the number of bytes read
*/
private int readRawData (ByteBuffer buffer) throws IOException{
// Read all the available data on the socket channel
int read=1;
int startPos = buffer.position();
while (read>0){
read = socketChannel.read(buffer);
if (read==-1){
// Channel closed
throw new IOException ("channel closed");
}
}
return buffer.position()-startPos;
}
/**
* Send data to the socket
* @param buffer the buffer to send
* @throws IOException in case of channel closed
*/
public void writeData (ByteBuffer buffer) throws IOException {
ByteBuffer bufferToWrite = buffer;
// Crypting data if needed
if (sslEngine!=null){
ByteBuffer myNetData = ByteBuffer.allocate(sslSession.getPacketBufferSize());
result = sslEngine.wrap(buffer, myNetData);
status = result.getStatus();
hsStatus = result.getHandshakeStatus();
bufferToWrite = myNetData;
bufferToWrite.flip();
}
// Write encrypted data
writeRawData(bufferToWrite);
}
/**
* Write raw data without encrypting them
* @param buffer the buffer of data to write
* @throws IOException if the connection has been closed
*/
private void writeRawData(ByteBuffer buffer)throws IOException{
// Write all the data into the socket channel
while (buffer.hasRemaining()){
int written = socketChannel.write(buffer);
/*<dbg>System.out.println("[SocketConnection - writeRawData] size written:"+written);</dbg>*/
if (written==-1){
// Channel closed
throw new IOException ("channel closed");
}
else if (written==0){
// No bytes written
// If there are still data it maybe because the client is too slow so we wait a little
if (buffer.hasRemaining()){
try {
Thread.sleep(500);
}
catch (Exception e){
}
}
}
}
}
/**
* Do all the outstanding handshake tasks in the current Thread.
*/
private SSLEngineResult.HandshakeStatus doTasks() {
Runnable runnable;
while ((runnable = sslEngine.getDelegatedTask()) != null) {
runnable.run();
}
return sslEngine.getHandshakeStatus();
}
}
Any help would be appreciated; thanks in advance !
Nicolas.