DIRAPI-373: Implement SASL integrity and confidentiality layer
diff --git a/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapClientApiOsgiTest.java b/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapClientApiOsgiTest.java
index 797bd5d..71e7262 100644
--- a/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapClientApiOsgiTest.java
+++ b/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapClientApiOsgiTest.java
@@ -22,6 +22,8 @@
 
 import org.apache.commons.pool2.PooledObjectFactory;
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.apache.directory.api.ldap.codec.api.SaslFilter;
+import org.apache.directory.api.ldap.model.constants.SaslQoP;
 import org.apache.directory.ldap.client.api.DefaultPoolableLdapConnectionFactory;
 import org.apache.directory.ldap.client.api.Krb5LoginConfiguration;
 import org.apache.directory.ldap.client.api.LdapConnection;
@@ -63,6 +65,11 @@
         ldapConnectionPool.getLdapApiService();
         ldapConnectionPool.getTestOnBorrow();
         ldapConnectionPool.close();
+
+        // Test SaslFilter
+        SaslFilter.OFFSET.toString();
+        SaslQoP.AUTH_CONF.name();
+        org.apache.mina.core.buffer.IoBuffer.isUseDirectBuffer();
     }
 
 }
diff --git a/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapCodecCoreOsgiTest.java b/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapCodecCoreOsgiTest.java
index c92be35..b878b9f 100644
--- a/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapCodecCoreOsgiTest.java
+++ b/integ-osgi/src/test/java/org/apache/directory/api/osgi/ApiLdapCodecCoreOsgiTest.java
@@ -35,9 +35,10 @@
 import org.apache.directory.api.ldap.codec.actions.response.search.done.InitSearchResultDone;
 import org.apache.directory.api.ldap.codec.api.LdapApiService;
 import org.apache.directory.api.ldap.codec.api.LdapApiServiceFactory;
+import org.apache.directory.api.ldap.codec.api.SaslFilter;
 import org.apache.directory.api.ldap.codec.search.AndFilter;
 import org.apache.directory.api.ldap.codec.search.SubstringFilter;
-import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.constants.SaslQoP;
 import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
 import org.apache.directory.api.ldap.model.message.controls.SortRequest;
 import org.junit.Test;
@@ -74,6 +75,11 @@
         new SubstringFilter();
 
         new SearchRequestImpl();
+
+        // Test SaslFilter
+        SaslFilter.OFFSET.toString();
+        SaslQoP.AUTH_CONF.name();
+        org.apache.mina.core.buffer.IoBuffer.isUseDirectBuffer();
     }
 
 
diff --git a/ldap/client/api/pom.xml b/ldap/client/api/pom.xml
index 227a204..88a892c 100644
--- a/ldap/client/api/pom.xml
+++ b/ldap/client/api/pom.xml
@@ -179,10 +179,12 @@
               org.apache.directory.api.ldap.model.schema.syntaxCheckers;version=${project.version},
               org.apache.directory.api.ldap.schema.manager.impl;version=${project.version},
               org.apache.directory.api.util;version=${project.version},
+              org.apache.mina.core.buffer;version=${mina.core.version},
               org.apache.mina.core.filterchain;version=${mina.core.version},
               org.apache.mina.core.future;version=${mina.core.version},
               org.apache.mina.core.service;version=${mina.core.version},
               org.apache.mina.core.session;version=${mina.core.version},
+              org.apache.mina.core.write;version=${mina.core.version},
               org.apache.mina.filter;version=${mina.core.version},
               org.apache.mina.filter.codec;version=${mina.core.version},
               org.apache.mina.filter.ssl;version=${mina.core.version},
diff --git a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/LdapNetworkConnection.java b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/LdapNetworkConnection.java
index 26c4db2..08b9395 100644
--- a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/LdapNetworkConnection.java
+++ b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/LdapNetworkConnection.java
@@ -66,6 +66,7 @@
 import org.apache.directory.api.ldap.codec.api.LdapDecoder;
 import org.apache.directory.api.ldap.codec.api.LdapMessageContainer;
 import org.apache.directory.api.ldap.codec.api.MessageEncoderException;
+import org.apache.directory.api.ldap.codec.api.SaslFilter;
 import org.apache.directory.api.ldap.codec.api.SchemaBinaryAttributeDetector;
 import org.apache.directory.api.ldap.extras.controls.ad.TreeDelete;
 import org.apache.directory.api.ldap.extras.controls.ad.TreeDeleteImpl;
@@ -167,6 +168,7 @@
 import org.apache.directory.ldap.client.api.future.ResponseFuture;
 import org.apache.directory.ldap.client.api.future.SearchFuture;
 import org.apache.mina.core.filterchain.IoFilter;
+import org.apache.mina.core.filterchain.IoFilterChain;
 import org.apache.mina.core.future.CloseFuture;
 import org.apache.mina.core.future.ConnectFuture;
 import org.apache.mina.core.future.WriteFuture;
@@ -195,6 +197,7 @@
  */
 public class LdapNetworkConnection extends AbstractLdapConnection implements LdapAsyncConnection
 {
+
     /** logger for reporting errors that might not be handled properly upstream */
     private static final Logger LOG = LoggerFactory.getLogger( LdapNetworkConnection.class );
 
@@ -236,12 +239,18 @@
      */
     private List<ConnectionClosedEventListener> conCloseListeners;
 
-    /** The Ldap codec protocol filter */
+    /** The LDAP codec protocol filter */
     private IoFilter ldapProtocolFilter = new ProtocolCodecFilter( codec.getProtocolCodecFactory() );
 
-    /** the SslFilter key */
+    /** The LDAP coded protocol filter key */
+    private static final String LDAP_CODEC_FILTER_KEY = "ldapCodec";
+
+    /** The SslFilter key */
     private static final String SSL_FILTER_KEY = "sslFilter";
 
+    /** The SaslFilter key */
+    private static final String SASL_FILTER_KEY = "saslFilter";
+
     /** The exception stored in the session if we've got one */
     private static final String EXCEPTION_KEY = "sessionException";
 
@@ -517,7 +526,7 @@
         }
 
         // Add the codec to the chain
-        connector.getFilterChain().addLast( "ldapCodec", ldapProtocolFilter );
+        connector.getFilterChain().addLast( LDAP_CODEC_FILTER_KEY, ldapProtocolFilter );
 
         // If we use SSL, we have to add the SslFilter to the chain
         if ( config.isUseSsl() )
@@ -4879,6 +4888,26 @@
 
 
     /**
+     * Adds a {@link SaslFilter} to the session's filter chain.
+     * 
+     * @param saslClient The initialized SASL client
+     * 
+     * @throws LdapException
+     */
+    private void addSaslFilter( SaslClient saslClient ) throws LdapException
+    {
+        IoFilterChain filterChain = ioSession.getFilterChain();
+        if ( filterChain.contains( SASL_FILTER_KEY ) )
+        {
+            filterChain.remove( SASL_FILTER_KEY );
+        }
+
+        SaslFilter saslFilter = new SaslFilter( saslClient );
+        filterChain.addBefore( LDAP_CODEC_FILTER_KEY, SASL_FILTER_KEY, saslFilter );
+    }
+
+
+    /**
      * Adds {@link SslFilter} to the IOConnector or IOSession's filter chain
      * 
      * @throws LdapException If the SSL filter addition failed
@@ -5130,6 +5159,15 @@
                 }
             }
 
+            /*
+             * Install the SASL filter when the SASL auth is complete.
+             * This adds the security layer if it was negotiated.
+             */
+            if ( sc.isComplete() )
+            {
+                addSaslFilter( sc );
+            }
+
             bindFuture.set( bindResponse );
 
             return bindFuture;
diff --git a/ldap/codec/core/pom.xml b/ldap/codec/core/pom.xml
index ab0be9d..8b1ee29 100644
--- a/ldap/codec/core/pom.xml
+++ b/ldap/codec/core/pom.xml
@@ -140,6 +140,7 @@
               org.apache.directory.api.asn1.ber.tlv;version=${project.version},
               org.apache.directory.api.asn1.util;version=${project.version},
               org.apache.directory.api.i18n;version=${project.version},
+              org.apache.directory.api.ldap.model.constants;version=${project.version},
               org.apache.directory.api.ldap.model.entry;version=${project.version},
               org.apache.directory.api.ldap.model.exception;version=${project.version},
               org.apache.directory.api.ldap.model.filter;version=${project.version},
@@ -150,6 +151,12 @@
               org.apache.directory.api.ldap.model.url;version=${project.version},
               org.apache.directory.api.util;version=${project.version},
               org.apache.directory.api.util.exception;version=${project.version},
+              org.apache.mina.core.buffer;version=${mina.core.version},
+              org.apache.mina.core.filterchain;version=${mina.core.version},
+              org.apache.mina.core.future;version=${mina.core.version},
+              org.apache.mina.core.service;version=${mina.core.version},
+              org.apache.mina.core.session;version=${mina.core.version},
+              org.apache.mina.core.write;version=${mina.core.version},
               org.apache.mina.filter.codec;version=${mina.core.version},
               org.apache.mina.util;version=${mina.core.version},
               org.osgi.framework;version="[1.0.0,2.0.0)",
diff --git a/ldap/codec/core/src/main/java/org/apache/directory/api/ldap/codec/api/SaslFilter.java b/ldap/codec/core/src/main/java/org/apache/directory/api/ldap/codec/api/SaslFilter.java
new file mode 100644
index 0000000..63091ac
--- /dev/null
+++ b/ldap/codec/core/src/main/java/org/apache/directory/api/ldap/codec/api/SaslFilter.java
@@ -0,0 +1,305 @@
+/*
+ *  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.directory.api.ldap.codec.api;
+
+
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+import javax.security.sasl.SaslServer;
+
+import org.apache.directory.api.ldap.model.constants.SaslQoP;
+import org.apache.mina.core.buffer.IoBuffer;
+import org.apache.mina.core.filterchain.IoFilterAdapter;
+import org.apache.mina.core.session.IoSession;
+import org.apache.mina.core.write.DefaultWriteRequest;
+import org.apache.mina.core.write.WriteRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * An {@link IoFilterAdapter} that handles integrity and confidentiality protection
+ * for a SASL bound session.  The SaslFilter must be constructed with a SASL
+ * context that has completed SASL negotiation.  Some SASL mechanisms, such as
+ * CRAM-MD5, only support authentication and thus do not need this filter.  DIGEST-MD5
+ * and GSSAPI do support message integrity and confidentiality and, therefore,
+ * do need this filter.
+ * 
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class SaslFilter extends IoFilterAdapter
+{
+    private static final Logger LOG = LoggerFactory.getLogger( SaslFilter.class );
+
+    /**
+     * A session attribute key that makes next one write request bypass
+     * this filter (not adding a security layer).  This is a marker attribute,
+     * which means that you can put whatever as its value. ({@link Boolean#TRUE}
+     * is preferred.)  The attribute is automatically removed from the session
+     * attribute map as soon as {@link IoSession#write(Object)} is invoked,
+     * and therefore should be put again if you want to make more messages
+     * bypass this filter.
+     */
+    public static final String DISABLE_SECURITY_LAYER_ONCE = SaslFilter.class.getName() + ".DisableSecurityLayerOnce";
+
+    /**
+     * A session attribute key that holds the received bytes of partially received
+     * SASL message.
+     */
+    public static final String BYTES = SaslFilter.class.getName() + ".Buffer";
+
+    /**
+     * A session attribute key that holds the offset of partially received
+     * SASL message.
+     */
+    public static final String OFFSET = SaslFilter.class.getName() + ".Offset";
+
+    /** The SASL client, only set if the filter is used at the client side. */
+    private final SaslClient saslClient;
+
+    /** The SASL server, only set if the filter is used at the server side. */
+    private final SaslServer saslServer;
+
+    /** True if a security layer has been negotiated */
+    private boolean hasSecurityLayer;
+
+    /** The negotiated max buffer size */
+    private int maxBufferSize;
+
+    /**
+     * Creates a new instance of SaslFilter.  The SaslFilter must be constructed
+     * with a SASL client that has completed SASL negotiation.  The SASL client
+     * will be used to provide message integrity and, optionally, message
+     * confidentiality.
+     *
+     * @param saslClient The initialized SASL client.
+     */
+    public SaslFilter( SaslClient saslClient )
+    {
+        if ( saslClient == null )
+        {
+            throw new IllegalArgumentException();
+        }
+
+        this.saslServer = null;
+        this.saslClient = saslClient;
+        initHasSecurityLayer( ( String ) saslClient.getNegotiatedProperty( Sasl.QOP ) );
+        initMaxBuffer( ( String ) saslClient.getNegotiatedProperty( Sasl.MAX_BUFFER ) );
+    }
+
+
+    /**
+     * Creates a new instance of SaslFilter.  The SaslFilter must be constructed
+     * with a SASL server that has completed SASL negotiation.  The SASL server
+     * will be used to provide message integrity and, optionally, message
+     * confidentiality.
+     *
+     * @param saslClient The initialized SASL server.
+     */
+    public SaslFilter( SaslServer saslServer )
+    {
+        if ( saslServer == null )
+        {
+            throw new IllegalArgumentException();
+        }
+
+        this.saslClient = null;
+        this.saslServer = saslServer;
+        initHasSecurityLayer( ( String ) saslServer.getNegotiatedProperty( Sasl.QOP ) );
+        initMaxBuffer( ( String ) saslServer.getNegotiatedProperty( Sasl.MAX_BUFFER ) );
+    }
+
+
+    private void initHasSecurityLayer( String qop )
+    {
+        this.hasSecurityLayer = ( qop != null && ( qop.equals( SaslQoP.AUTH_INT.getValue() ) || qop
+            .equals( SaslQoP.AUTH_CONF.getValue() ) ) );
+    }
+
+
+    private void initMaxBuffer( String maxBuffer )
+    {
+        this.maxBufferSize = maxBuffer != null ? Integer.parseInt( maxBuffer ) : 65536;
+    }
+
+
+    @Override
+    public synchronized void messageReceived( NextFilter nextFilter, IoSession session, Object message )
+        throws SaslException
+    {
+        LOG.debug( "Message received:  {}", message );
+
+        if ( !hasSecurityLayer )
+        {
+            LOG.debug( "Will not use SASL on received message." );
+            nextFilter.messageReceived( session, message );
+            return;
+        }
+
+        /*
+         * Unwrap the data for mechanisms that support QoP (DIGEST-MD5, GSSAPI).
+         */
+        IoBuffer buf = ( IoBuffer ) message;
+        while ( buf.hasRemaining() )
+        {
+            /*
+             * Check for a previously received partial SASL message which is stored in the session.
+             * Otherwise read the first 4 bytes which is the length and allocate the bytes.
+             * Ensure the buffer size doesn't exceed the negotiated max buffer size.
+             */
+            byte[] bytes = ( byte[] ) session.getAttribute( BYTES, null );
+            int offset = ( int ) session.getAttribute( OFFSET, -1 );
+            if ( bytes == null )
+            {
+                int bufferSize = buf.getInt();
+                if ( bufferSize > maxBufferSize )
+                {
+                    throw new IllegalStateException(
+                        bufferSize + " exceeds the negotiated receive buffer size limit: " + maxBufferSize );
+                }
+                bytes = new byte[bufferSize];
+                offset = 0;
+            }
+
+            /*
+             * Get the buffer as bytes. Handle the case that only a part of the SASL message was received.
+             */
+            int length = Math.min( bytes.length - offset, buf.remaining() );
+            buf.get( bytes, offset, length );
+
+            /*
+             * Check if the full SASL message was received. If not store the partially received data in
+             * the session so it can be resumed when the next message is received.
+             */
+            offset += length;
+            if ( offset < bytes.length )
+            {
+                LOG.debug( "Partial SASL message received:  {}/{}", offset, bytes.length );
+                session.setAttribute( BYTES, bytes );
+                session.setAttribute( OFFSET, offset );
+                break;
+            }
+
+            /*
+             * Unwrap the SASL message and forward it to the next filter.
+             */
+            LOG.debug( "Will use SASL to unwrap received message of length:  {}", bytes.length );
+            byte[] token = unwrap( bytes, 0, bytes.length );
+            nextFilter.messageReceived( session, IoBuffer.wrap( token ) );
+
+            /*
+             * Finally clear the session attributes.
+             */
+            session.removeAttribute( BYTES );
+            session.removeAttribute( OFFSET );
+        }
+    }
+
+
+    @Override
+    public synchronized void filterWrite( NextFilter nextFilter, IoSession session, WriteRequest writeRequest )
+        throws SaslException
+    {
+        LOG.debug( "Filtering write request:  {}", writeRequest );
+
+        /*
+         * Check if security layer processing should be disabled once.
+         */
+        if ( session.containsAttribute( DISABLE_SECURITY_LAYER_ONCE ) )
+        {
+            // Remove the marker attribute because it is temporary.
+            LOG.debug( "Disabling SaslFilter once; will not use SASL on write request." );
+            session.removeAttribute( DISABLE_SECURITY_LAYER_ONCE );
+            nextFilter.filterWrite( session, writeRequest );
+            return;
+        }
+
+        if ( !hasSecurityLayer )
+        {
+            LOG.debug( "Will not use SASL on write request." );
+            nextFilter.filterWrite( session, writeRequest );
+            return;
+        }
+
+        /*
+         * Wrap the data for mechanisms that support QoP (DIGEST-MD5, GSSAPI).
+         */
+
+        /*
+         * Get the buffer as bytes.
+         */
+        IoBuffer buf = ( IoBuffer ) writeRequest.getMessage();
+        int bufferLength = buf.remaining();
+        byte[] bufferBytes = new byte[bufferLength];
+        buf.get( bufferBytes );
+
+        LOG.info( "Will use SASL to wrap message of length:  {}", bufferLength );
+
+        /*
+         * Ensure to not send larger SASL message than negotiated.
+         */
+        int max = maxBufferSize - 200;
+        for ( int offset = 0; offset < bufferLength; offset += max )
+        {
+            int length = Math.min( bufferLength - offset, max );
+            byte[] saslLayer = wrap( bufferBytes, offset, length );
+
+            /*
+             * Prepend 4 byte length.
+             */
+            IoBuffer saslLayerBuffer = IoBuffer.allocate( 4 + saslLayer.length );
+            saslLayerBuffer.putInt( saslLayer.length );
+            saslLayerBuffer.put( saslLayer );
+            saslLayerBuffer.position( 0 );
+            saslLayerBuffer.limit( 4 + saslLayer.length );
+
+            LOG.debug( "Sending encrypted token of length {}.", saslLayerBuffer.limit() );
+            nextFilter.filterWrite( session, new DefaultWriteRequest( saslLayerBuffer, writeRequest.getFuture() ) );
+        }
+    }
+
+
+    private byte[] wrap( byte[] buffer, int offset, int length ) throws SaslException
+    {
+        if ( saslClient != null )
+        {
+            return saslClient.wrap( buffer, offset, length );
+        }
+        else
+        {
+            return saslServer.wrap( buffer, offset, length );
+        }
+    }
+
+
+    private byte[] unwrap( byte[] buffer, int offset, int length ) throws SaslException
+    {
+        if ( saslClient != null )
+        {
+            return saslClient.unwrap( buffer, offset, length );
+        }
+        else
+        {
+            return saslServer.unwrap( buffer, offset, length );
+        }
+    }
+
+}