/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2008 Sun Microsystems, Inc. */ package org.opends.server.extensions; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import javax.security.sasl.Sasl; import javax.security.sasl.SaslException; import org.opends.server.api.ClientConnection; import org.opends.server.api.ConnectionSecurityProvider; import org.opends.server.config.ConfigEntry; import org.opends.server.config.ConfigException; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.protocols.ldap.LDAPClientConnection; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.DisconnectReason; import org.opends.server.types.InitializationException; import static org.opends.server.loggers.debug.DebugLogger.*; /** * This class provides an implementation of a connection security provider that * provides SASL confidentiality/integrity between the server and client. * */ public class SASLSecurityProvider extends ConnectionSecurityProvider { // The tracer object for the debug logger. private static final DebugTracer TRACER = getTracer(); //The client connection associated with this provider. private ClientConnection connection; //The socket channel associated with this provider. private SocketChannel sockChannel; //The SASL context associated with the provider private SASLContext saslContext; //The number of bytes in the length buffer. private final int lengthSize = 4; //A byte buffer used to hold the length of the clear buffer. private ByteBuffer lengthBuf = ByteBuffer.allocate(lengthSize); //The SASL mechanism name. private String name; /** * Create a SASL security provider with the specified parameters that is * capable of processing a confidentiality/integrity SASL connection. * * @param connection The client connection to read/write the bytes. * @param name The SASL mechanism name. * @param saslContext The SASL context to process the data through. */ public SASLSecurityProvider(ClientConnection connection, String name, SASLContext saslContext) { super(); this.connection = connection; this.name = name; this.saslContext = saslContext; this.sockChannel = ((LDAPClientConnection) connection).getSocketChannel(); } /** * {@inheritDoc} */ @Override public void disconnect(boolean connectionValid) { this.saslContext.dispose(); } /** * {@inheritDoc} */ @Override public void finalizeConnectionSecurityProvider() { } /** * {@inheritDoc} */ @Override public int getClearBufferSize() { return 0; } /** * {@inheritDoc} */ @Override public int getEncodedBufferSize() { return 0; } /** * {@inheritDoc} */ @Override public String getSecurityMechanismName() { return name; } /** * {@inheritDoc} */ @Override public void initializeConnectionSecurityProvider(ConfigEntry configEntry) throws ConfigException, InitializationException { this.connection = null; this.sockChannel = null; } /** * {@inheritDoc} */ @Override public boolean isSecure() { return true; } /** * {@inheritDoc} */ @Override public ConnectionSecurityProvider newInstance(ClientConnection clientConn, SocketChannel socketChannel) { return new SASLSecurityProvider(clientConn, null,null); } /** * Return the clear buffer length as determined by processing the first * 4 bytes of the specified buffer. * * @param byteBuf The buffer to examine the first 4 bytes of. * @return The size of the clear buffer. */ private int getBufLength(ByteBuffer byteBuf) { int answer = 0; byte[] buf = byteBuf.array(); for (int i = 0; i < lengthSize; i++) { answer <<= 8; answer |= ((int)buf[i] & 0xff); } return answer; } /** * Read from the socket channel into the specified byte buffer the * number of bytes specified in the total parameter. * * @param byteBuf The byte buffer to put the bytes in. * @param total The total number of bytes to read from the socket channel. * @return The number of bytes read, 0 or -1. * @throws IOException If an error occurred reading the socket channel. */ private int readAll(ByteBuffer byteBuf, int total) throws IOException { int count = 0; while(sockChannel.isOpen() && total > 0) { count = sockChannel.read(byteBuf); if(count == -1) return -1; if(count == 0) return 0; total -= count; } if(total > 0) return -1; else return byteBuf.position(); } /** * {@inheritDoc} */ @Override public boolean readData() throws DirectoryException { int recvBufSize = saslContext.getBufSize(Sasl.MAX_BUFFER); try { while(true) { lengthBuf.clear(); int readResult = readAll(lengthBuf, lengthSize); if (readResult == -1) { //Client connection has been closed. Disconnect and return. connection.disconnect(DisconnectReason.CLIENT_DISCONNECT, false, null); return false; } else if(readResult == 0) return true; int bufLength = getBufLength(lengthBuf); if(bufLength > recvBufSize) { connection.disconnect(DisconnectReason.CLIENT_DISCONNECT, false, null); return false; } ByteBuffer readBuf = ByteBuffer.allocate(bufLength); readResult = readAll(readBuf, bufLength); if (readResult == -1) { //Client connection has been closed. Disconnect and return. connection.disconnect(DisconnectReason.CLIENT_DISCONNECT, false, null); return false; } else if (readResult == 0) return true; byte[] inBytes = readBuf.array(); byte[] clearBytes = saslContext.unwrap(inBytes, 0, inBytes.length); ByteBuffer clearBuffer = ByteBuffer.wrap(clearBytes); if (!connection.processDataRead(clearBuffer)) return false; } } catch (SaslException saslEx) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, saslEx); } //Error trying to unwrap the data. connection.disconnect(DisconnectReason.IO_ERROR, false, null); return false; } catch (IOException ioe) { // An error occurred while trying to communicate with the client. // Disconnect and return. if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ioe); } connection.disconnect(DisconnectReason.IO_ERROR, false, null); return false; } } /** * Creates a buffer suitable to send to the client using the specified * clear byte array, offset and length of the bytes to wrap. * * @param clearBytes The clear byte array to send to the client. * @param offSet An offset into the byte array to start the wrap at. * @param len The length of the bytes to wrap in the byte array. * @throws SaslException If the wrap of the bytes fails. */ private ByteBuffer createSendBuffer(byte[] clearBytes, int offSet, int len) throws SaslException { byte[] wrapBytes = saslContext.wrap(clearBytes, offSet, len); byte[] outBuf = new byte[wrapBytes.length + lengthSize]; writeBufLen(outBuf, wrapBytes.length); System.arraycopy(wrapBytes, 0, outBuf, lengthSize, wrapBytes.length); return ByteBuffer.wrap(outBuf); } /** * Writes the specified len parameter into the buffer in a form that can * be sent over a network to the client. * * @param buf The buffer to hold the length bytes. * @param len The length to encode. */ private void writeBufLen(byte[] buf, int len) { for (int i = 3; i >= 0; i--) { buf[i] = (byte)(len & 0xff); len >>>= 8; } } /** * {@inheritDoc} */ @Override() public boolean writeData(ByteBuffer clearData) { int maxSendBufSize = saslContext.getBufSize(Sasl.RAW_SEND_SIZE); int clearLength = clearData.limit(); try { if(clearLength < maxSendBufSize) { ByteBuffer sendBuffer = createSendBuffer(clearData.array(),0,clearLength); return writeChannel(sendBuffer); } else { byte[] clearBytes = clearData.array(); for(int i=0; i < clearLength; i += maxSendBufSize) { int totLength = (clearLength - i) < maxSendBufSize ? (clearLength - i) : maxSendBufSize; ByteBuffer sendBuffer = createSendBuffer(clearBytes, i, totLength); if(!writeChannel(sendBuffer)) return false; } } } catch (SaslException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } connection.disconnect(DisconnectReason.IO_ERROR, false, null); return false; } return true; } /** * Write the specified byte buffer to the socket channel. * * @param buffer The byte buffer to write to the socket channel. * @return {@code true} if the byte buffer was successfully written to the * socket channel, or, {@code false} if not. */ private boolean writeChannel(ByteBuffer buffer) { try { while (buffer.hasRemaining()) { int bytesWritten = sockChannel.write(buffer); if (bytesWritten < 0) { connection.disconnect(DisconnectReason.CLIENT_DISCONNECT, false, null); return false; } else if (bytesWritten == 0) { return writeWithTimeout(buffer); } } } catch (IOException ioe) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ioe); } connection.disconnect(DisconnectReason.IO_ERROR, false, null); return false; } return true; } /** * Writes the specified byte buffer parameter to the socket channel waiting * for a period of time if the buffer cannot be written immediately. * * @param buffer The byte buffer to write to the channel. * @return {@code true} if the bytes were sent, or, {@code false} otherwise. * @throws IOException If an IO error occurs while writing the bytes. */ private boolean writeWithTimeout(ByteBuffer buffer) throws IOException { long startTime = System.currentTimeMillis(); long waitTime = connection.getMaxBlockedWriteTimeLimit(); if (waitTime <= 0) { // We won't support an infinite time limit, so fall back to using // five minutes, which is a very long timeout given that we're // blocking a worker thread. waitTime = 300000L; } long stopTime = startTime + waitTime; Selector selector = connection.getWriteSelector(); if (selector == null) { // The client connection does not provide a selector, so we'll // fall back to a more inefficient way that will work without a // selector. while (buffer.hasRemaining() && (System.currentTimeMillis() < stopTime)) { if (sockChannel.write(buffer) < 0) { // The client connection has been closed connection.disconnect(DisconnectReason.CLIENT_DISCONNECT, false, null); return false; } } if (buffer.hasRemaining()) { // If we've gotten here, then the write timed out. // Terminate the client connection. connection.disconnect(DisconnectReason.IO_TIMEOUT, false, null); return false; } return true; } // Register with the selector for handling write operations. SelectionKey key = sockChannel.register(selector, SelectionKey.OP_WRITE); try { selector.select(waitTime); while (buffer.hasRemaining()) { long currentTime = System.currentTimeMillis(); if (currentTime >= stopTime) { // We've been blocked for too long connection.disconnect(DisconnectReason.IO_TIMEOUT, false, null); return false; } else { waitTime = stopTime - currentTime; } Iterator iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey k = iterator.next(); if (k.isWritable()) { int bytesWritten = sockChannel.write(buffer); if (bytesWritten < 0) { // The client connection has been closed. connection.disconnect( DisconnectReason.CLIENT_DISCONNECT, false, null); return false; } iterator.remove(); } } if (buffer.hasRemaining()) { selector.select(waitTime); } } return true; } finally { if (key.isValid()) { key.cancel(); selector.selectNow(); } } } /** * Return if the underlying SASL context is active or not. The SASL context * may still be negotiating a multi-stage SASL bind and is not ready to * process confidentiality or integrity data yet. * * @return {@code true} if the underlying SASL context is active or ready * to process confidentiality/integrity messages, or, {@code false} * if not. */ public boolean isActive() { return saslContext.isBindComplete(); } }