blob: 74843a8385ea4beb3c192e8ed7888a1cf6131299 [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.session;
import static org.apache.mina.session.AttributeKey.createKey;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Queue;
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.SSLException;
import javax.net.ssl.SSLSession;
import org.apache.mina.api.IoClient;
import org.apache.mina.api.IoSession;
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 final static 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;
/**
* 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);
/** An empty buffer used during the handshake phase */
private static final ByteBuffer HANDSHAKE_BUFFER = ByteBuffer.allocate(1024);
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;
}
boolean isHanshaking() {
return sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING;
}
/**
* Initialize the SSL handshake.
*
* @throws SSLException If the underlying SSLEngine handshake initialization failed
*/
/* no qualifier */void init() throws SSLException {
if (sslEngine != null) {
// We already have a SSL engine created, no need to create a new one
return;
}
LOGGER.debug("{} Initializing the SSL Helper", 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);
handshakeStatus = result.getHandshakeStatus();
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 NEED_TASK:
Runnable task;
while ((task = sslEngine.getDelegatedTask()) != null) {
task.run();
}
handshakeStatus = sslEngine.getHandshakeStatus();
break;
case NEED_WRAP:
result = sslEngine.wrap(EMPTY_BUFFER, appBuffer);
handshakeStatus = result.getHandshakeStatus();
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(readBuffer);
writeRequest.setMessage(appBuffer);
session.enqueueWriteRequest(writeRequest);
break;
}
}
}
if (tempBuffer.remaining() > 0) {
previous = duplicate(tempBuffer);
} else {
previous = null;
}
readBuffer.clear();
}
/**
* Process the application data encryption for a session.
* @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(IoSession session, Object message, Queue<WriteRequest> writeQueue) {
ByteBuffer buf = (ByteBuffer) message;
ByteBuffer appBuffer = ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize());
try {
while (true) {
// 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 BUFFER_UNDERFLOW:
case CLOSED:
break;
case OK:
// We are done. Flip the buffer and push it to the write queue.
appBuffer.flip();
WriteRequest request = new DefaultWriteRequest(appBuffer);
return request;
}
}
} catch (SSLException se) {
throw new IllegalStateException(se.getMessage());
}
}
}