blob: 83cce1b271dbf989883df334e8fe5ddd48dc1bf8 [file] [log] [blame]
/*
* 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.mina.transport.nio;
import static org.apache.mina.session.AttributeKey.createKey;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.net.ssl.SSLContext;
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 javax.net.ssl.SSLSession;
import org.apache.mina.api.IoClient;
import org.apache.mina.api.IoSession;
import org.apache.mina.session.AbstractIoSession;
import org.apache.mina.session.AttributeKey;
import org.apache.mina.session.DefaultWriteRequest;
import org.apache.mina.session.WriteRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An helper class used to manage everything related to SSL/TLS establishment and management.
*
* @author <a href="http://mina.apache.org">Apache MINA Project</a>
*/
public class SslHelper {
/** A logger for this class */
private static final Logger LOGGER = LoggerFactory.getLogger(SslHelper.class);
/** The SSL engine instance */
private SSLEngine sslEngine;
/** The SSLContext instance */
private final SSLContext sslContext;
/** The current session */
private final IoSession session;
/**
* The internal secure state of the session.
* CREDENTIALS_NOT_YET_AVAILABLE: the session is currently handskaking, application messages
* will be queued before being encrypted and sent.
* CREDENTAILS_AVAILABLE: the session has completed handshake, application messages
* can be encrypted and sent as they are submitted.
* NO_CREDENTIALS: secure credentials are removed from the session, application messages
* are not encrypted anymore.
*
*/
enum State {
CREDENTIALS_NOT_YET_AVAILABLE,
CREDENTAILS_AVAILABLE,
NO_CREDENTIALS
}
private State state = State.CREDENTIALS_NOT_YET_AVAILABLE;
/**
* The list of applications messages queued because submitted while the initial handshake was
* not yet finished.
*/
private ConcurrentLinkedQueue<WriteRequest> messages = new ConcurrentLinkedQueue<WriteRequest>();
/**
* A session attribute key that should be set to an {@link InetSocketAddress}. Setting this attribute causes
* {@link SSLContext#createSSLEngine(String, int)} to be called passing the hostname and port of the
* {@link InetSocketAddress} to get an {@link SSLEngine} instance. If not set {@link SSLContext#createSSLEngine()}
* will be called.<br/>
* Using this feature {@link SSLSession} objects may be cached and reused when in client mode.
*
* @see SSLContext#createSSLEngine(String, int)
*/
public static final AttributeKey<InetSocketAddress> PEER_ADDRESS = createKey(InetSocketAddress.class,
"internal_peerAddress");
public static final AttributeKey<Boolean> WANT_CLIENT_AUTH = createKey(Boolean.class, "internal_wantClientAuth");
public static final AttributeKey<Boolean> NEED_CLIENT_AUTH = createKey(Boolean.class, "internal_needClientAuth");
/** Incoming buffer accumulating bytes read from the channel */
/** An empty buffer used during the handshake phase */
private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
private ByteBuffer previous = null;
/**
* Create a new SSL Handler.
*
* @param session The associated session
*/
public SslHelper(IoSession session, SSLContext sslContext) {
this.session = session;
this.sslContext = sslContext;
}
/**
* @return The associated session
*/
/* no qualifier */IoSession getSession() {
return session;
}
/**
* @return The associated SSLEngine
*/
/* no qualifier */SSLEngine getEngine() {
return sslEngine;
}
/**
* Return the state (credentials state) of the session.
*
* @return the credentials state
*/
State getState() {
return state;
}
boolean isHanshaking() {
return sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING;
}
public boolean isActive() {
return state != State.NO_CREDENTIALS;
}
/**
* Initialize the SSL handshake.
*
*/
public void init() {
if (sslEngine != null) {
// We already have a SSL engine created, no need to create a new one
return;
}
LOGGER.debug("{} Initializing the SSLEngine", session);
InetSocketAddress peer = session.getAttribute(PEER_ADDRESS, null);
// Create the SSL engine here
if (peer == null) {
sslEngine = sslContext.createSSLEngine();
} else {
sslEngine = sslContext.createSSLEngine(peer.getHostName(), peer.getPort());
}
// Initialize the engine in client mode if necessary
sslEngine.setUseClientMode(session.getService() instanceof IoClient);
// Initialize the different SslEngine modes
if (!sslEngine.getUseClientMode()) {
// Those parameters are only valid when in server mode
boolean needClientAuth = session.getAttribute(NEED_CLIENT_AUTH, false);
boolean wantClientAuth = session.getAttribute(WANT_CLIENT_AUTH, false);
// The WantClientAuth supersede the NeedClientAuth, if set.
if (needClientAuth) {
sslEngine.setNeedClientAuth(true);
}
if (wantClientAuth) {
sslEngine.setWantClientAuth(true);
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("{} SSL Handler Initialization done.", session);
}
}
/**
* Duplicate a byte buffer for storing it into this context for future use.
*
* @param buffer the buffer to duplicate
* @return the newly allocated buffer
*/
private ByteBuffer duplicate(ByteBuffer buffer) {
ByteBuffer newBuffer = ByteBuffer.allocateDirect(buffer.remaining() * 2);
newBuffer.put(buffer);
newBuffer.flip();
return newBuffer;
}
/**
* Accumulate the given buffer into the current context. Allocation is performed only if needed.
*
* @param buffer the buffer to accumulate
* @return the accumulated buffer
*/
private ByteBuffer accumulate(ByteBuffer buffer) {
if (previous.capacity() - previous.remaining() > buffer.remaining()) {
int oldPosition = previous.position();
previous.position(previous.limit());
previous.limit(previous.limit() + buffer.remaining());
previous.put(buffer);
previous.position(oldPosition);
} else {
ByteBuffer newPrevious = ByteBuffer.allocateDirect((previous.remaining() + buffer.remaining()) * 2);
newPrevious.put(previous);
newPrevious.put(buffer);
newPrevious.flip();
previous = newPrevious;
}
return previous;
}
/**
* Process a read ByteBuffer over a secured connection, or during the SSL/TLS Handshake.
*
* @param session The session we are processing a read for
* @param readBuffer The data we get from the channel
* @throws SSLException If the unwrapping or handshaking failed
*/
public void processRead(AbstractIoSession session, ByteBuffer readBuffer) throws SSLException {
ByteBuffer tempBuffer;
if (previous != null) {
tempBuffer = accumulate(readBuffer);
} else {
tempBuffer = readBuffer;
}
boolean done = false;
SSLEngineResult result;
ByteBuffer appBuffer = ByteBuffer.allocateDirect(sslEngine.getSession().getApplicationBufferSize());
HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus();
while (!done) {
switch (handshakeStatus) {
case NEED_UNWRAP:
case NOT_HANDSHAKING:
case FINISHED:
result = sslEngine.unwrap(tempBuffer, appBuffer);
processResult(session, handshakeStatus, result);
switch (result.getStatus()) {
case BUFFER_UNDERFLOW:
/* we need more data */
done = true;
break;
case BUFFER_OVERFLOW:
/* resize output buffer */
appBuffer = ByteBuffer.allocateDirect(appBuffer.capacity() * 2);
break;
case OK:
if ((handshakeStatus == HandshakeStatus.NOT_HANDSHAKING) && (result.bytesProduced() > 0)) {
appBuffer.flip();
session.processMessageReceived(appBuffer);
}
break;
case CLOSED:
break;
}
if (sslEngine != null) {
handshakeStatus = sslEngine.getHandshakeStatus();
} else {
done = true;
}
break;
case NEED_TASK:
Runnable task;
while ((task = sslEngine.getDelegatedTask()) != null) {
task.run();
}
handshakeStatus = sslEngine.getHandshakeStatus();
break;
case NEED_WRAP:
result = sslEngine.wrap(EMPTY_BUFFER, appBuffer);
processResult(session, handshakeStatus, result);
switch (result.getStatus()) {
case BUFFER_OVERFLOW:
appBuffer = ByteBuffer.allocateDirect(appBuffer.capacity() * 2);
break;
case BUFFER_UNDERFLOW:
done = true;
break;
case CLOSED:
case OK:
appBuffer.flip();
WriteRequest writeRequest = new DefaultWriteRequest(appBuffer);
writeRequest.setMessage(appBuffer);
writeRequest.setSecureInternal(true);
session.enqueueWriteRequest(writeRequest);
break;
}
if (sslEngine != null) {
handshakeStatus = sslEngine.getHandshakeStatus();
} else {
done = true;
}
}
if (handshakeStatus == HandshakeStatus.FINISHED) {
state = State.CREDENTAILS_AVAILABLE;
}
}
if (tempBuffer.remaining() > 0) {
previous = duplicate(tempBuffer);
} else {
previous = null;
}
readBuffer.clear();
}
/**
* Process the close event from the SSL engine. If the closed event has not been
* processed, then send an event.
*
* @param session the {@link AbstractIoSession} MINA internal IO session
*/
void switchToNoSecure(AbstractIoSession session) {
if (state != State.NO_CREDENTIALS) {
session.processSecureClosed();
state = State.NO_CREDENTIALS;
sslEngine = null;
}
}
/**
* Process the session handshake status and the last operation result in order to
* update the internal state and propagate handshake related events.
*
* @param session the {@link AbstractIoSession} MINA internal IO session
* @param sessionStatus the last session handshake status
* @param operationStatus the returned operation status
*/
private void processResult(AbstractIoSession session, HandshakeStatus sessionStatus, SSLEngineResult result) {
LOGGER.debug("handshake status:" + sessionStatus + " engine result:" + result);
switch (sessionStatus) {
case NEED_TASK:
case NEED_UNWRAP:
case NEED_WRAP:
if (result.getHandshakeStatus() == HandshakeStatus.FINISHED) {
state = State.CREDENTAILS_AVAILABLE;
session.processHandshakeCompleted();
for(WriteRequest request : messages) {
session.enqueueWriteRequest(request);
}
messages.clear();
}
if (result.getStatus() == Status.CLOSED) {
switchToNoSecure(session);
}
break;
case FINISHED:
case NOT_HANDSHAKING:
if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
session.processHandshakeStarted();
}
break;
}
}
/**
* Process the application data encryption for a session. As the SSLEngine is record
* oriented, then depending on the message size, this may lead to several encrypted
* messages to be generated. So, if n messages are generated, the first n-1 will
* be queued and the last one will be returned. It will be automatically added
* to the end of the queue by the called because a non empty queue will be
* detected.
*
* @param session The session sending encrypted data to the peer.
* @param message The message to encrypt
* @param writeQueue The queue in which the encrypted buffer will be written
* @return The written WriteRequest
*/
/** No qualifier */
WriteRequest processWrite(AbstractIoSession session, Object message, Queue<WriteRequest> writeQueue) {
WriteRequest request = null;
switch (state) {
case CREDENTAILS_AVAILABLE:
ByteBuffer buf = (ByteBuffer) message;
ByteBuffer appBuffer = ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize());
try {
boolean done = false;
while (!done) {
// Encrypt the message
SSLEngineResult result = sslEngine.wrap(buf, appBuffer);
switch (result.getStatus()) {
case BUFFER_OVERFLOW:
// Increase the buffer size as needed
appBuffer = ByteBuffer.allocate(appBuffer.capacity() + 4096);
break;
case CLOSED:
switchToNoSecure(session);
done = true;
break;
case BUFFER_UNDERFLOW:
case OK:
// We are done. Flip the buffer and push it to the write queue.
appBuffer.flip();
done = buf.remaining() == 0;
if (done) {
request = new DefaultWriteRequest(appBuffer);
} else {
writeQueue.offer(new DefaultWriteRequest(appBuffer));
appBuffer = ByteBuffer.allocateDirect(appBuffer.capacity());
}
break;
}
}
} catch (SSLException se) {
throw new IllegalStateException(se.getMessage());
}
break;
case CREDENTIALS_NOT_YET_AVAILABLE:
messages.add(new DefaultWriteRequest(message));
break;
case NO_CREDENTIALS:
request = new DefaultWriteRequest(message);
break;
}
return request;
}
public void beginHandshake() throws IOException {
if (sslEngine != null) {
((AbstractIoSession)session).processHandshakeStarted();
sslEngine.beginHandshake();
processRead((AbstractIoSession) session, EMPTY_BUFFER);
}
}
public void close() throws IOException {
if (sslEngine != null) {
sslEngine.closeOutbound();
processRead((AbstractIoSession) session, EMPTY_BUFFER);
}
}
}