All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.java_websocket.SSLSocketChannel Maven / Gradle / Ivy

There is a newer version: 1.5.7
Show newest version
/*
 * Copyright (c) 2010-2020 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.interfaces.ISSLChannel;
import org.java_websocket.util.ByteBufferUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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, ISSLChannel { /** * Logger instance * * @since 1.4.0 */ private final Logger log = LoggerFactory.getLogger(SSLSocketChannel.class); /** * The underlying socket channel */ private final SocketChannel socketChannel; /** * The engine which will be used for un-/wrapping of buffers */ private final SSLEngine engine; /** * 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 ); } } else { try { socketChannel.close(); } catch ( IOException e ) { log.error("Exception during the closing of the channel", e); } } } @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 ) { log.error("SSLExcpetion during unwrap", e); 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 ); return read(dst); 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: *

*

    *
  • 1. wrap: ClientHello
  • *
  • 2. unwrap: ServerHello/Cert/ServerHelloDone
  • *
  • 3. wrap: ClientKeyExchange
  • *
  • 4. wrap: ChangeCipherSpec
  • *
  • 5. wrap: Finished
  • *
  • 6. unwrap: ChangeCipherSpec
  • *
  • 7. unwrap: Finished
  • *
*

* Handshake is also used during the end of the session, in order to properly close the connection between the two peers. * A proper connection close will typically include the one peer sending a CLOSE message to another, and then wait for * the other's CLOSE message to close the transport link. The other peer from his perspective would read a CLOSE message * from his peer and then enter the handshake procedure to send his own CLOSE message as well. * * @return True if the connection handshake was successful or false if an error occurred. * @throws IOException - if an error occurs during read/write to the socket channel. */ private boolean doHandshake() throws IOException { SSLEngineResult result; HandshakeStatus handshakeStatus; // NioSslPeer's fields myAppData and peerAppData are supposed to be large enough to hold all message data the peer // will send and expects to receive from the other peer respectively. Since the messages to be exchanged will usually be less // than 16KB long the capacity of these fields should also be smaller. Here we initialize these two local buffers // to be used for the handshake, while keeping client's buffers at the same size. int appBufferSize = engine.getSession().getApplicationBufferSize(); myAppData = ByteBuffer.allocate( appBufferSize ); peerAppData = ByteBuffer.allocate( appBufferSize ); myNetData.clear(); peerNetData.clear(); handshakeStatus = engine.getHandshakeStatus(); boolean handshakeComplete = false; while( !handshakeComplete) { switch(handshakeStatus) { case FINISHED: handshakeComplete = !this.peerNetData.hasRemaining(); if (handshakeComplete) return true; socketChannel.write(this.peerNetData); break; case NEED_UNWRAP: if( socketChannel.read( peerNetData ) < 0 ) { if( engine.isInboundDone() && engine.isOutboundDone() ) { return false; } try { engine.closeInbound(); } catch ( SSLException e ) { //Ignore, cant do anything against this exception } engine.closeOutbound(); // After closeOutbound the engine will be set to WRAP state, in order to try to send a close message to the client. handshakeStatus = engine.getHandshakeStatus(); break; } peerNetData.flip(); try { result = engine.unwrap( peerNetData, peerAppData ); peerNetData.compact(); handshakeStatus = result.getHandshakeStatus(); } catch ( SSLException sslException ) { engine.closeOutbound(); handshakeStatus = engine.getHandshakeStatus(); break; } switch(result.getStatus()) { case OK: break; case BUFFER_OVERFLOW: // Will occur when peerAppData's capacity is smaller than the data derived from peerNetData's unwrap. peerAppData = enlargeApplicationBuffer( peerAppData ); break; case BUFFER_UNDERFLOW: // Will occur either when no data was read from the peer or when the peerNetData buffer was too small to hold all peer's data. peerNetData = handleBufferUnderflow( peerNetData ); break; case CLOSED: if( engine.isOutboundDone() ) { return false; } else { engine.closeOutbound(); handshakeStatus = engine.getHandshakeStatus(); break; } default: throw new IllegalStateException( "Invalid SSL status: " + result.getStatus() ); } break; case NEED_WRAP: myNetData.clear(); try { result = engine.wrap( myAppData, myNetData ); handshakeStatus = result.getHandshakeStatus(); } catch ( SSLException sslException ) { engine.closeOutbound(); handshakeStatus = engine.getHandshakeStatus(); break; } switch(result.getStatus()) { case OK: myNetData.flip(); while( myNetData.hasRemaining() ) { socketChannel.write( myNetData ); } break; case BUFFER_OVERFLOW: // Will occur if there is not enough space in myNetData buffer to write all the data that would be generated by the method wrap. // Since myNetData is set to session's packet size we should not get to this point because SSLEngine is supposed // to produce messages smaller or equal to that, but a general handling would be the following: 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: try { myNetData.flip(); while( myNetData.hasRemaining() ) { socketChannel.write( myNetData ); } // At this point the handshake status will probably be NEED_UNWRAP so we make sure that peerNetData is clear to read. peerNetData.clear(); } catch ( Exception e ) { handshakeStatus = engine.getHandshakeStatus(); } break; default: throw new IllegalStateException( "Invalid SSL status: " + result.getStatus() ); } break; case NEED_TASK: Runnable task; while( ( task = engine.getDelegatedTask() ) != null ) { executor.execute( task ); } handshakeStatus = engine.getHandshakeStatus(); break; case NOT_HANDSHAKING: break; default: throw new IllegalStateException( "Invalid SSL status: " + handshakeStatus ); } } return true; } /** * Enlarging a packet buffer (peerNetData or myNetData) * * @param buffer the buffer to enlarge * @return the enlarged buffer */ private ByteBuffer enlargePacketBuffer( ByteBuffer buffer ) { return enlargeBuffer( buffer, engine.getSession().getPacketBufferSize() ); } /** * Enlarging a packet buffer (peerAppData or myAppData) * * @param buffer the buffer to enlarge * @return the enlarged buffer */ private ByteBuffer enlargeApplicationBuffer( ByteBuffer buffer ) { return enlargeBuffer( buffer, engine.getSession().getApplicationBufferSize() ); } /** * Compares 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 ) { log.error( "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(); } @Override public SSLEngine getSSLEngine() { return engine; } }