Skip to Main Content

Java Security

Announcement

For appeals, questions and feedback about Oracle Forums, please email oracle-forums-moderators_us@oracle.com. Technical questions should be asked in the appropriate category. Thank you!

Problem reading data from a nio socket using SSLEngine

843811Apr 12 2006 — edited Apr 17 2006
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.
Comments
Locked Post
New comments cannot be posted to this locked post.
Post Details
Locked on May 15 2006
Added on Apr 12 2006
7 comments
1,327 views