| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.tomcat.util.net; |
| |
| import java.io.EOFException; |
| import java.io.IOException; |
| import java.net.SocketTimeoutException; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.SelectionKey; |
| import java.nio.channels.Selector; |
| import java.nio.channels.SocketChannel; |
| |
| import javax.net.ssl.SSLEngine; |
| import javax.net.ssl.SSLEngineResult; |
| import javax.net.ssl.SSLEngineResult.HandshakeStatus; |
| import javax.net.ssl.SSLEngineResult.Status; |
| import javax.net.ssl.SSLException; |
| |
| import org.apache.juli.logging.Log; |
| import org.apache.juli.logging.LogFactory; |
| |
| /** |
| * |
| * Implementation of a secure socket channel |
| * @version 1.0 |
| */ |
| |
| public class SecureNioChannel extends NioChannel { |
| |
| protected static final Log log = LogFactory.getLog(SecureNioChannel.class); |
| |
| protected ByteBuffer netInBuffer; |
| protected ByteBuffer netOutBuffer; |
| |
| protected SSLEngine sslEngine; |
| |
| protected boolean handshakeComplete = false; |
| protected HandshakeStatus handshakeStatus; //gets set by handshake |
| |
| protected boolean closed = false; |
| protected boolean closing = false; |
| |
| protected NioSelectorPool pool; |
| |
| public SecureNioChannel(SocketChannel channel, SSLEngine engine, |
| ApplicationBufferHandler bufHandler, NioSelectorPool pool) throws IOException { |
| super(channel,bufHandler); |
| this.sslEngine = engine; |
| int appBufSize = sslEngine.getSession().getApplicationBufferSize(); |
| int netBufSize = sslEngine.getSession().getPacketBufferSize(); |
| //allocate network buffers - TODO, add in optional direct non-direct buffers |
| if ( netInBuffer == null ) netInBuffer = ByteBuffer.allocateDirect(netBufSize); |
| if ( netOutBuffer == null ) netOutBuffer = ByteBuffer.allocateDirect(netBufSize); |
| |
| //selector pool for blocking operations |
| this.pool = pool; |
| |
| //ensure that the application has a large enough read/write buffers |
| //by doing this, we should not encounter any buffer overflow errors |
| bufHandler.expand(bufHandler.getReadBuffer(), appBufSize); |
| bufHandler.expand(bufHandler.getWriteBuffer(), appBufSize); |
| reset(); |
| } |
| |
| public void reset(SSLEngine engine) throws IOException { |
| this.sslEngine = engine; |
| reset(); |
| } |
| @Override |
| public void reset() throws IOException { |
| super.reset(); |
| netOutBuffer.position(0); |
| netOutBuffer.limit(0); |
| netInBuffer.position(0); |
| netInBuffer.limit(0); |
| handshakeComplete = false; |
| closed = false; |
| closing = false; |
| //initiate handshake |
| sslEngine.beginHandshake(); |
| handshakeStatus = sslEngine.getHandshakeStatus(); |
| } |
| |
| @Override |
| public int getBufferSize() { |
| int size = super.getBufferSize(); |
| size += netInBuffer!=null?netInBuffer.capacity():0; |
| size += netOutBuffer!=null?netOutBuffer.capacity():0; |
| return size; |
| } |
| |
| |
| //=========================================================================================== |
| // NIO SSL METHODS |
| //=========================================================================================== |
| /** |
| * Flush the channel. |
| * |
| * @param block Should a blocking write be used? |
| * @param s |
| * @param timeout |
| * @return <code>true</code> if the network buffer has been flushed out and |
| * is empty else <code>false</code> |
| * @throws IOException |
| */ |
| @Override |
| public boolean flush(boolean block, Selector s, long timeout) |
| throws IOException { |
| if (!block) { |
| flush(netOutBuffer); |
| } else { |
| pool.write(netOutBuffer, this, s, timeout,block); |
| } |
| return !netOutBuffer.hasRemaining(); |
| } |
| |
| /** |
| * Flushes the buffer to the network, non blocking |
| * @param buf ByteBuffer |
| * @return boolean true if the buffer has been emptied out, false otherwise |
| * @throws IOException |
| */ |
| protected boolean flush(ByteBuffer buf) throws IOException { |
| int remaining = buf.remaining(); |
| if ( remaining > 0 ) { |
| int written = sc.write(buf); |
| return written >= remaining; |
| }else { |
| return true; |
| } |
| } |
| |
| /** |
| * Performs SSL handshake, non blocking, but performs NEED_TASK on the same thread.<br> |
| * Hence, you should never call this method using your Acceptor thread, as you would slow down |
| * your system significantly.<br> |
| * The return for this operation is 0 if the handshake is complete and a positive value if it is not complete. |
| * In the event of a positive value coming back, reregister the selection key for the return values interestOps. |
| * @param read boolean - true if the underlying channel is readable |
| * @param write boolean - true if the underlying channel is writable |
| * @return int - 0 if hand shake is complete, otherwise it returns a SelectionKey interestOps value |
| * @throws IOException |
| */ |
| @Override |
| public int handshake(boolean read, boolean write) throws IOException { |
| if ( handshakeComplete ) return 0; //we have done our initial handshake |
| |
| if (!flush(netOutBuffer)) return SelectionKey.OP_WRITE; //we still have data to write |
| |
| SSLEngineResult handshake = null; |
| |
| while (!handshakeComplete) { |
| switch ( handshakeStatus ) { |
| case NOT_HANDSHAKING: { |
| //should never happen |
| throw new IOException(sm.getString("channel.nio.ssl.notHandshaking")); |
| } |
| case FINISHED: { |
| //we are complete if we have delivered the last package |
| handshakeComplete = !netOutBuffer.hasRemaining(); |
| //return 0 if we are complete, otherwise we still have data to write |
| return handshakeComplete?0:SelectionKey.OP_WRITE; |
| } |
| case NEED_WRAP: { |
| //perform the wrap function |
| try { |
| handshake = handshakeWrap(write); |
| } catch (SSLException e) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("channel.nio.ssl.wrapException"), e); |
| } |
| handshake = handshakeWrap(write); |
| } |
| if (handshake.getStatus() == Status.OK) { |
| if (handshakeStatus == HandshakeStatus.NEED_TASK) |
| handshakeStatus = tasks(); |
| } else if (handshake.getStatus() == Status.CLOSED) { |
| flush(netOutBuffer); |
| return -1; |
| } else { |
| //wrap should always work with our buffers |
| throw new IOException(sm.getString("channel.nio.ssl.unexpectedStatusDuringWrap", handshake.getStatus())); |
| } |
| if ( handshakeStatus != HandshakeStatus.NEED_UNWRAP || (!flush(netOutBuffer)) ) { |
| //should actually return OP_READ if we have NEED_UNWRAP |
| return SelectionKey.OP_WRITE; |
| } |
| //fall down to NEED_UNWRAP on the same call, will result in a |
| //BUFFER_UNDERFLOW if it needs data |
| } |
| //$FALL-THROUGH$ |
| case NEED_UNWRAP: { |
| //perform the unwrap function |
| handshake = handshakeUnwrap(read); |
| if ( handshake.getStatus() == Status.OK ) { |
| if (handshakeStatus == HandshakeStatus.NEED_TASK) |
| handshakeStatus = tasks(); |
| } else if ( handshake.getStatus() == Status.BUFFER_UNDERFLOW ){ |
| //read more data, reregister for OP_READ |
| return SelectionKey.OP_READ; |
| } else { |
| throw new IOException(sm.getString("channel.nio.ssl.unexpectedStatusDuringWrap", handshakeStatus)); |
| }//switch |
| break; |
| } |
| case NEED_TASK: { |
| handshakeStatus = tasks(); |
| break; |
| } |
| default: throw new IllegalStateException(sm.getString("channel.nio.ssl.invalidStatus", handshakeStatus)); |
| } |
| } |
| // Handshake is complete if this point is reached |
| return 0; |
| } |
| |
| /** |
| * Force a blocking handshake to take place for this key. |
| * This requires that both network and application buffers have been emptied out prior to this call taking place, or a |
| * IOException will be thrown. |
| * @param timeout - timeout in milliseconds for each socket operation |
| * @throws IOException - if an IO exception occurs or if application or network buffers contain data |
| * @throws SocketTimeoutException - if a socket operation timed out |
| */ |
| @SuppressWarnings("null") // key cannot be null |
| public void rehandshake(long timeout) throws IOException { |
| //validate the network buffers are empty |
| if (netInBuffer.position() > 0 && netInBuffer.position()<netInBuffer.limit()) throw new IOException(sm.getString("channel.nio.ssl.netInputNotEmpty")); |
| if (netOutBuffer.position() > 0 && netOutBuffer.position()<netOutBuffer.limit()) throw new IOException(sm.getString("channel.nio.ssl.netOutputNotEmpty")); |
| if (getBufHandler().getReadBuffer().position()>0 && getBufHandler().getReadBuffer().position()<getBufHandler().getReadBuffer().limit()) throw new IOException(sm.getString("channel.nio.ssl.appInputNotEmpty")); |
| if (getBufHandler().getWriteBuffer().position()>0 && getBufHandler().getWriteBuffer().position()<getBufHandler().getWriteBuffer().limit()) throw new IOException(sm.getString("channel.nio.ssl.appOutputNotEmpty")); |
| reset(); |
| boolean isReadable = true; |
| boolean isWriteable = true; |
| boolean handshaking = true; |
| Selector selector = null; |
| SelectionKey key = null; |
| try { |
| while (handshaking) { |
| int hsStatus = this.handshake(isReadable, isWriteable); |
| switch (hsStatus) { |
| case -1 : throw new EOFException(sm.getString("channel.nio.ssl.eofDuringHandshake")); |
| case 0 : handshaking = false; break; |
| default : { |
| long now = System.currentTimeMillis(); |
| if (selector==null) { |
| synchronized (Selector.class) { |
| // Selector.open() isn't thread safe |
| // http://bugs.sun.com/view_bug.do?bug_id=6427854 |
| // Affects 1.6.0_29, fixed in 1.7.0_01 |
| selector = Selector.open(); |
| } |
| key = getIOChannel().register(selector, hsStatus); |
| } else { |
| key.interestOps(hsStatus); // null warning supressed |
| } |
| int keyCount = selector.select(timeout); |
| if (keyCount == 0 && ((System.currentTimeMillis()-now) >= timeout)) { |
| throw new SocketTimeoutException(sm.getString("channel.nio.ssl.timeoutDuringHandshake")); |
| } |
| isReadable = key.isReadable(); |
| isWriteable = key.isWritable(); |
| } |
| } |
| } |
| } catch (IOException x) { |
| throw x; |
| } catch (Exception cx) { |
| IOException x = new IOException(cx); |
| throw x; |
| } finally { |
| if (key!=null) try {key.cancel();} catch (Exception ignore) {} |
| if (selector!=null) try {selector.close();} catch (Exception ignore) {} |
| } |
| } |
| |
| |
| |
| /** |
| * Executes all the tasks needed on the same thread. |
| * @return HandshakeStatus |
| */ |
| protected SSLEngineResult.HandshakeStatus tasks() { |
| Runnable r = null; |
| while ( (r = sslEngine.getDelegatedTask()) != null) { |
| r.run(); |
| } |
| return sslEngine.getHandshakeStatus(); |
| } |
| |
| /** |
| * Performs the WRAP function |
| * @param doWrite boolean |
| * @return SSLEngineResult |
| * @throws IOException |
| */ |
| protected SSLEngineResult handshakeWrap(boolean doWrite) throws IOException { |
| //this should never be called with a network buffer that contains data |
| //so we can clear it here. |
| netOutBuffer.clear(); |
| //perform the wrap |
| SSLEngineResult result = sslEngine.wrap(bufHandler.getWriteBuffer(), netOutBuffer); |
| //prepare the results to be written |
| netOutBuffer.flip(); |
| //set the status |
| handshakeStatus = result.getHandshakeStatus(); |
| //optimization, if we do have a writable channel, write it now |
| if ( doWrite ) flush(netOutBuffer); |
| return result; |
| } |
| |
| /** |
| * Perform handshake unwrap |
| * @param doread boolean |
| * @return SSLEngineResult |
| * @throws IOException |
| */ |
| protected SSLEngineResult handshakeUnwrap(boolean doread) throws IOException { |
| |
| if (netInBuffer.position() == netInBuffer.limit()) { |
| //clear the buffer if we have emptied it out on data |
| netInBuffer.clear(); |
| } |
| if ( doread ) { |
| //if we have data to read, read it |
| int read = sc.read(netInBuffer); |
| if (read == -1) throw new IOException(sm.getString("channel.nio.ssl.eofDuringHandshake")); |
| } |
| SSLEngineResult result; |
| boolean cont = false; |
| //loop while we can perform pure SSLEngine data |
| do { |
| //prepare the buffer with the incoming data |
| netInBuffer.flip(); |
| //call unwrap |
| result = sslEngine.unwrap(netInBuffer, bufHandler.getReadBuffer()); |
| //compact the buffer, this is an optional method, wonder what would happen if we didn't |
| netInBuffer.compact(); |
| //read in the status |
| handshakeStatus = result.getHandshakeStatus(); |
| if ( result.getStatus() == SSLEngineResult.Status.OK && |
| result.getHandshakeStatus() == HandshakeStatus.NEED_TASK ) { |
| //execute tasks if we need to |
| handshakeStatus = tasks(); |
| } |
| //perform another unwrap? |
| cont = result.getStatus() == SSLEngineResult.Status.OK && |
| handshakeStatus == HandshakeStatus.NEED_UNWRAP; |
| }while ( cont ); |
| return result; |
| } |
| |
| /** |
| * Sends a SSL close message, will not physically close the connection here. |
| * <br>To close the connection, you could do something like |
| * <pre><code> |
| * close(); |
| * while (isOpen() && !myTimeoutFunction()) Thread.sleep(25); |
| * if ( isOpen() ) close(true); //forces a close if you timed out |
| * </code></pre> |
| * @throws IOException if an I/O error occurs |
| * @throws IOException if there is data on the outgoing network buffer and |
| * we are unable to flush it |
| */ |
| @Override |
| public void close() throws IOException { |
| if (closing) return; |
| closing = true; |
| sslEngine.closeOutbound(); |
| |
| if (!flush(netOutBuffer)) { |
| throw new IOException(sm.getString("channel.nio.ssl.remainingDataDuringClose")); |
| } |
| //prep the buffer for the close message |
| netOutBuffer.clear(); |
| //perform the close, since we called sslEngine.closeOutbound |
| SSLEngineResult handshake = sslEngine.wrap(getEmptyBuf(), netOutBuffer); |
| //we should be in a close state |
| if (handshake.getStatus() != SSLEngineResult.Status.CLOSED) { |
| throw new IOException(sm.getString("channel.nio.ssl.invalidCloseState")); |
| } |
| //prepare the buffer for writing |
| netOutBuffer.flip(); |
| //if there is data to be written |
| flush(netOutBuffer); |
| |
| //is the channel closed? |
| closed = (!netOutBuffer.hasRemaining() && (handshake.getHandshakeStatus() != HandshakeStatus.NEED_WRAP)); |
| } |
| |
| /** |
| * Force a close, can throw an IOException |
| * @param force boolean |
| * @throws IOException |
| */ |
| @Override |
| public void close(boolean force) throws IOException { |
| try { |
| close(); |
| }finally { |
| if ( force || closed ) { |
| closed = true; |
| sc.socket().close(); |
| sc.close(); |
| } |
| } |
| } |
| |
| /** |
| * Reads a sequence of bytes from this channel into the given buffer. |
| * |
| * @param dst The buffer into which bytes are to be transferred |
| * @return The number of bytes read, possibly zero, or <tt>-1</tt> if the |
| * channel has reached end-of-stream |
| * @throws IOException If some other I/O error occurs |
| * @throws IllegalArgumentException if the destination buffer is different |
| * than bufHandler.getReadBuffer() |
| */ |
| @Override |
| public int read(ByteBuffer dst) throws IOException { |
| //if we want to take advantage of the expand function, make sure we only use the ApplicationBufferHandler's buffers |
| if ( dst != bufHandler.getReadBuffer() ) throw new IllegalArgumentException(sm.getString("channel.nio.ssl.invalidBuffer")); |
| //are we in the middle of closing or closed? |
| if ( closing || closed) return -1; |
| //did we finish our handshake? |
| if (!handshakeComplete) throw new IllegalStateException(sm.getString("channel.nio.ssl.incompleteHandshake")); |
| |
| //read from the network |
| int netread = sc.read(netInBuffer); |
| //did we reach EOF? if so send EOF up one layer. |
| if (netread == -1) return -1; |
| |
| //the data read |
| int read = 0; |
| //the SSL engine result |
| SSLEngineResult unwrap; |
| do { |
| //prepare the buffer |
| netInBuffer.flip(); |
| //unwrap the data |
| unwrap = sslEngine.unwrap(netInBuffer, dst); |
| //compact the buffer |
| netInBuffer.compact(); |
| |
| if ( unwrap.getStatus()==Status.OK || unwrap.getStatus()==Status.BUFFER_UNDERFLOW ) { |
| //we did receive some data, add it to our total |
| read += unwrap.bytesProduced(); |
| //perform any tasks if needed |
| if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK) tasks(); |
| //if we need more network data, then bail out for now. |
| if ( unwrap.getStatus() == Status.BUFFER_UNDERFLOW ) break; |
| }else if ( unwrap.getStatus()==Status.BUFFER_OVERFLOW && read>0 ) { |
| //buffer overflow can happen, if we have read data, then |
| //empty out the dst buffer before we do another read |
| break; |
| }else { |
| //here we should trap BUFFER_OVERFLOW and call expand on the buffer |
| //for now, throw an exception, as we initialized the buffers |
| //in the constructor |
| throw new IOException(sm.getString("channel.nio.ssl.unwrapFail", unwrap.getStatus())); |
| } |
| } while ( (netInBuffer.position() != 0)); //continue to unwrapping as long as the input buffer has stuff |
| return (read); |
| } |
| |
| /** |
| * Writes a sequence of bytes to this channel from the given buffer. |
| * |
| * @param src The buffer from which bytes are to be retrieved |
| * @return The number of bytes written, possibly zero |
| * @throws IOException If some other I/O error occurs |
| */ |
| @Override |
| public int write(ByteBuffer src) throws IOException { |
| checkInterruptStatus(); |
| if (src == this.netOutBuffer) { |
| //we can get here through a recursive call |
| //by using the NioBlockingSelector |
| int written = sc.write(src); |
| return written; |
| } else { |
| // Are we closing or closed? |
| if (closing || closed) { |
| throw new IOException(sm.getString("channel.nio.ssl.closing")); |
| } |
| |
| if (!flush(netOutBuffer)) { |
| // We haven't emptied out the buffer yet |
| return 0; |
| } |
| |
| // The data buffer is empty, we can reuse the entire buffer. |
| netOutBuffer.clear(); |
| |
| SSLEngineResult result = sslEngine.wrap(src, netOutBuffer); |
| // The number of bytes written |
| int written = result.bytesConsumed(); |
| netOutBuffer.flip(); |
| |
| if (result.getStatus() == Status.OK) { |
| if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) tasks(); |
| } else { |
| throw new IOException(sm.getString("channel.nio.ssl.wrapFail", result.getStatus())); |
| } |
| |
| // Force a flush |
| flush(netOutBuffer); |
| |
| return written; |
| } |
| } |
| |
| @Override |
| public int getOutboundRemaining() { |
| return netOutBuffer.remaining(); |
| } |
| |
| @Override |
| public boolean flushOutbound() throws IOException { |
| int remaining = netOutBuffer.remaining(); |
| flush(netOutBuffer); |
| int remaining2= netOutBuffer.remaining(); |
| return remaining2 < remaining; |
| } |
| |
| |
| /** |
| * Callback interface to be able to expand buffers |
| * when buffer overflow exceptions happen |
| */ |
| public static interface ApplicationBufferHandler { |
| public ByteBuffer expand(ByteBuffer buffer, int remaining); |
| public ByteBuffer getReadBuffer(); |
| public ByteBuffer getWriteBuffer(); |
| } |
| |
| @Override |
| public ApplicationBufferHandler getBufHandler() { |
| return bufHandler; |
| } |
| |
| @Override |
| public boolean isHandshakeComplete() { |
| return handshakeComplete; |
| } |
| |
| @Override |
| public boolean isClosing() { |
| return closing; |
| } |
| |
| public SSLEngine getSslEngine() { |
| return sslEngine; |
| } |
| |
| public ByteBuffer getEmptyBuf() { |
| return emptyBuf; |
| } |
| |
| public void setBufHandler(ApplicationBufferHandler bufHandler) { |
| this.bufHandler = bufHandler; |
| } |
| } |