/* * Copyright (c) 2010-2017 Nathan Rajlich * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ package org.java_websocket; import org.java_websocket.util.ByteBufferUtils; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import java.io.IOException; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.util.concurrent.ExecutorService; /** * A class that represents an SSL/TLS peer, and can be extended to create a client or a server. * * It makes use of the JSSE framework, and specifically the {@link SSLEngine} logic, which * is described by Oracle as "an advanced API, not appropriate for casual use", since * it requires the user to implement much of the communication establishment procedure himself. * More information about it can be found here: http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SSLEngine * * {@link SSLSocketChannel} implements the handshake protocol, required to establish a connection between two peers, * which is common for both client and server and provides the abstract {@link SSLSocketChannel#read(ByteBuffer)} and * {@link SSLSocketChannel#write(ByteBuffer)} (String)} methods, that need to be implemented by the specific SSL/TLS peer * that is going to extend this class. * * @author Alex Karnezis *
* Modified by marci4 to allow the usage as a ByteChannel *
* Permission for usage recieved at May 25, 2017 by Alex Karnezis */ public class SSLSocketChannel implements WrappedByteChannel, ByteChannel { /** * The underlaying socket channel */ private final SocketChannel socketChannel; /** * The engine which will be used for un-/wrapping of buffers */ private final SSLEngine engine; /** * The selection key for this socket channel * Used to set interestOP SelectionKey.OP_WRITE for the underlying channel */ private SelectionKey selectionKey; /** * Will contain this peer's application data in plaintext, that will be later encrypted * using {@link SSLEngine#wrap(ByteBuffer, ByteBuffer)} and sent to the other peer. This buffer can typically * be of any size, as long as it is large enough to contain this peer's outgoing messages. * If this peer tries to send a message bigger than buffer's capacity a {@link BufferOverflowException} * will be thrown. */ private ByteBuffer myAppData; /** * Will contain this peer's encrypted data, that will be generated after {@link SSLEngine#wrap(ByteBuffer, ByteBuffer)} * is applied on {@link SSLSocketChannel#myAppData}. It should be initialized using {@link SSLSession#getPacketBufferSize()}, * which returns the size up to which, SSL/TLS packets will be generated from the engine under a session. * All SSLEngine network buffers should be sized at least this large to avoid insufficient space problems when performing wrap and unwrap calls. */ private ByteBuffer myNetData; /** * Will contain the other peer's (decrypted) application data. It must be large enough to hold the application data * from any peer. Can be initialized with {@link SSLSession#getApplicationBufferSize()} for an estimation * of the other peer's application data and should be enlarged if this size is not enough. */ private ByteBuffer peerAppData; /** * Will contain the other peer's encrypted data. The SSL/TLS protocols specify that implementations should produce packets containing at most 16 KB of plaintext, * so a buffer sized to this value should normally cause no capacity problems. However, some implementations violate the specification and generate large records up to 32 KB. * If the {@link SSLEngine#unwrap(ByteBuffer, ByteBuffer)} detects large inbound packets, the buffer sizes returned by SSLSession will be updated dynamically, so the this peer * should check for overflow conditions and enlarge the buffer using the session's (updated) buffer size. */ private ByteBuffer peerNetData; /** * Will be used to execute tasks that may emerge during handshake in parallel with the server's main thread. */ private ExecutorService executor; public SSLSocketChannel( SocketChannel inputSocketChannel, SSLEngine inputEngine, ExecutorService inputExecutor, SelectionKey key ) throws IOException { if( inputSocketChannel == null || inputEngine == null || executor == inputExecutor ) throw new IllegalArgumentException( "parameter must not be null" ); this.socketChannel = inputSocketChannel; this.engine = inputEngine; this.executor = inputExecutor; myNetData = ByteBuffer.allocate( engine.getSession().getPacketBufferSize() ); peerNetData = ByteBuffer.allocate( engine.getSession().getPacketBufferSize() ); this.engine.beginHandshake(); if( doHandshake() ) { if( key != null ) { key.interestOps( key.interestOps() | SelectionKey.OP_WRITE ); this.selectionKey = key; } } else { try { socketChannel.close(); } catch ( IOException e ) { e.printStackTrace(); } } } @Override public synchronized int read( ByteBuffer dst ) throws IOException { if( !dst.hasRemaining() ) { return 0; } if( peerAppData.hasRemaining() ) { peerAppData.flip(); return ByteBufferUtils.transferByteBuffer( peerAppData, dst ); } peerNetData.compact(); int bytesRead = socketChannel.read( peerNetData ); /** * If bytesRead are 0 put we still have some data in peerNetData still to an unwrap (for testcase 1.1.6) */ if( bytesRead > 0 || peerNetData.hasRemaining() ) { peerNetData.flip(); while( peerNetData.hasRemaining() ) { peerAppData.compact(); SSLEngineResult result; try { result = engine.unwrap( peerNetData, peerAppData ); } catch ( SSLException e ) { e.printStackTrace(); throw e; } switch(result.getStatus()) { case OK: peerAppData.flip(); return ByteBufferUtils.transferByteBuffer( peerAppData, dst ); case BUFFER_UNDERFLOW: peerAppData.flip(); return ByteBufferUtils.transferByteBuffer( peerAppData, dst ); case BUFFER_OVERFLOW: peerAppData = enlargeApplicationBuffer( peerAppData ); break; case CLOSED: closeConnection(); dst.clear(); return -1; default: throw new IllegalStateException( "Invalid SSL status: " + result.getStatus() ); } } } else if( bytesRead < 0 ) { handleEndOfStream(); } ByteBufferUtils.transferByteBuffer( peerAppData, dst ); return bytesRead; } @Override public synchronized int write( ByteBuffer output ) throws IOException { int num = 0; while( output.hasRemaining() ) { // The loop has a meaning for (outgoing) messages larger than 16KB. // Every wrap call will remove 16KB from the original message and send it to the remote peer. myNetData.clear(); SSLEngineResult result = engine.wrap( output, myNetData ); switch(result.getStatus()) { case OK: myNetData.flip(); while( myNetData.hasRemaining() ) { num += socketChannel.write( myNetData ); } break; case BUFFER_OVERFLOW: myNetData = enlargePacketBuffer( myNetData ); break; case BUFFER_UNDERFLOW: throw new SSLException( "Buffer underflow occured after a wrap. I don't think we should ever get here." ); case CLOSED: closeConnection(); return 0; default: throw new IllegalStateException( "Invalid SSL status: " + result.getStatus() ); } } return num; } /** * Implements the handshake protocol between two peers, required for the establishment of the SSL/TLS connection. * During the handshake, encryption configuration information - such as the list of available cipher suites - will be exchanged * and if the handshake is successful will lead to an established SSL/TLS session. *
*
* A typical handshake will usually contain the following steps: **
sessionProposedCapacity with buffer's capacity. If buffer's capacity is smaller,
* returns a buffer with the proposed capacity. If it's equal or larger, returns a buffer
* with capacity twice the size of the initial one.
*
* @param buffer - the buffer to be enlarged.
* @param sessionProposedCapacity - the minimum size of the new buffer, proposed by {@link SSLSession}.
* @return A new buffer with a larger capacity.
*/
private ByteBuffer enlargeBuffer( ByteBuffer buffer, int sessionProposedCapacity ) {
if( sessionProposedCapacity > buffer.capacity() ) {
buffer = ByteBuffer.allocate( sessionProposedCapacity );
} else {
buffer = ByteBuffer.allocate( buffer.capacity() * 2 );
}
return buffer;
}
/**
* Handles {@link SSLEngineResult.Status#BUFFER_UNDERFLOW}. Will check if the buffer is already filled, and if there is no space problem
* will return the same buffer, so the client tries to read again. If the buffer is already filled will try to enlarge the buffer either to
* session's proposed size or to a larger capacity. A buffer underflow can happen only after an unwrap, so the buffer will always be a
* peerNetData buffer.
*
* @param buffer - will always be peerNetData buffer.
* @return The same buffer if there is no space problem or a new buffer with the same data but more space.
*/
private ByteBuffer handleBufferUnderflow( ByteBuffer buffer ) {
if( engine.getSession().getPacketBufferSize() < buffer.limit() ) {
return buffer;
} else {
ByteBuffer replaceBuffer = enlargePacketBuffer( buffer );
buffer.flip();
replaceBuffer.put( buffer );
return replaceBuffer;
}
}
/**
* This method should be called when this peer wants to explicitly close the connection
* or when a close message has arrived from the other peer, in order to provide an orderly shutdown.
*
* It first calls {@link SSLEngine#closeOutbound()} which prepares this peer to send its own close message and
* sets {@link SSLEngine} to the NEED_WRAP state. Then, it delegates the exchange of close messages
* to the handshake method and finally, it closes socket channel.
*
* @throws IOException if an I/O error occurs to the socket channel.
*/
private void closeConnection() throws IOException {
engine.closeOutbound();
try {
doHandshake();
} catch ( IOException e ) {
//Just ignore this exception since we are closing the connection already
}
socketChannel.close();
}
/**
* In addition to orderly shutdowns, an unorderly shutdown may occur, when the transport link (socket channel)
* is severed before close messages are exchanged. This may happen by getting an -1 or {@link IOException}
* when trying to read from the socket channel, or an {@link IOException} when trying to write to it.
* In both cases {@link SSLEngine#closeInbound()} should be called and then try to follow the standard procedure.
*
* @throws IOException if an I/O error occurs to the socket channel.
*/
private void handleEndOfStream() throws IOException {
try {
engine.closeInbound();
} catch ( Exception e ) {
System.err.println( "This engine was forced to close inbound, without having received the proper SSL/TLS close notification message from the peer, due to end of stream." );
}
closeConnection();
}
@Override
public boolean isNeedWrite() {
return false;
}
@Override
public void writeMore() throws IOException {
//Nothing to do since we write out all the data in a while loop
}
@Override
public boolean isNeedRead() {
return peerNetData.hasRemaining() || peerAppData.hasRemaining();
}
@Override
public int readMore( ByteBuffer dst ) throws IOException {
return read( dst );
}
@Override
public boolean isBlocking() {
return socketChannel.isBlocking();
}
@Override
public boolean isOpen() {
return socketChannel.isOpen();
}
@Override
public void close() throws IOException {
closeConnection();
}
}