org.apache.directory.studio.connection.core.io.jndi.JNDIConnectionWrapper.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.directory.studio.connection.core.io.jndi.JNDIConnectionWrapper.java

Source

/*
 *  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.studio.connection.core.io.jndi;

import java.io.File;
import java.io.IOException;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;

import javax.naming.CommunicationException;
import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.InsufficientResourcesException;
import javax.naming.InvalidNameException;
import javax.naming.Name;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
import javax.naming.ReferralException;
import javax.naming.ServiceUnavailableException;
import javax.naming.directory.Attributes;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.security.sasl.Sasl;

import org.apache.directory.api.util.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.model.constants.SaslQoP;
import org.apache.directory.api.ldap.model.constants.SaslSecurityStrength;
import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
import org.apache.directory.api.ldap.model.message.Referral;
import org.apache.directory.api.ldap.model.message.ReferralImpl;
import org.apache.directory.api.ldap.model.url.LdapUrl;
import org.apache.directory.studio.common.core.jobs.StudioProgressMonitor;
import org.apache.directory.studio.connection.core.Connection;
import org.apache.directory.studio.connection.core.Connection.AliasDereferencingMethod;
import org.apache.directory.studio.connection.core.Connection.ReferralHandlingMethod;
import org.apache.directory.studio.connection.core.ConnectionCoreConstants;
import org.apache.directory.studio.connection.core.ConnectionCorePlugin;
import org.apache.directory.studio.connection.core.ConnectionParameter;
import org.apache.directory.studio.connection.core.ConnectionParameter.AuthenticationMethod;
import org.apache.directory.studio.connection.core.IAuthHandler;
import org.apache.directory.studio.connection.core.ICredentials;
import org.apache.directory.studio.connection.core.IJndiLogger;
import org.apache.directory.studio.connection.core.Messages;
import org.apache.directory.studio.connection.core.Utils;
import org.apache.directory.studio.connection.core.io.ConnectionWrapper;
import org.apache.directory.studio.connection.core.io.ConnectionWrapperUtils;
import org.eclipse.core.runtime.Preferences;
import org.eclipse.osgi.util.NLS;

/**
 * A connection wrapper that uses JNDI.
 * 
 * - asychron + cancelable
 * - SSL certificate
 * - manages broken/closed connections
 * - delete old Rdn
 * - exception handling 
 * - referral handling
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class JNDIConnectionWrapper implements ConnectionWrapper {
    private static final String JAVA_NAMING_LDAP_DELETE_RDN = "java.naming.ldap.deleteRDN"; //$NON-NLS-1$

    private static final String AUTHMETHOD_NONE = "none"; //$NON-NLS-1$

    private static final String AUTHMETHOD_SIMPLE = "simple"; //$NON-NLS-1$

    private static final String AUTHMETHOD_DIGEST_MD5 = "DIGEST-MD5"; //$NON-NLS-1$

    private static final String AUTHMETHOD_CRAM_MD5 = "CRAM-MD5"; //$NON-NLS-1$

    private static final String AUTHMETHOD_GSSAPI = "GSSAPI"; //$NON-NLS-1$

    private static final String NO_CONNECTION = "No connection"; //$NON-NLS-1$

    private static final String JAVA_NAMING_SECURITY_SASL_REALM = "java.naming.security.sasl.realm"; //$NON-NLS-1$

    private static final String JAVA_NAMING_LDAP_FACTORY_SOCKET = "java.naming.ldap.factory.socket"; //$NON-NLS-1$

    private static final String COM_SUN_JNDI_DNS_TIMEOUT_RETRIES = "com.sun.jndi.dns.timeout.retries"; //$NON-NLS-1$

    private static final String COM_SUN_JNDI_DNS_TIMEOUT_INITIAL = "com.sun.jndi.dns.timeout.initial"; //$NON-NLS-1$

    private static final String COM_SUN_JNDI_LDAP_CONNECT_TIMEOUT = "com.sun.jndi.ldap.connect.timeout"; //$NON-NLS-1$

    private static final String JAVA_NAMING_LDAP_VERSION = "java.naming.ldap.version"; //$NON-NLS-1$

    private static final String JAVA_NAMING_LDAP_DEREF_ALIASES = "java.naming.ldap.derefAliases"; //$NON-NLS-1$

    private static final String JAVA_NAMING_LDAP_ATTRIBUTES_BINARY = "java.naming.ldap.attributes.binary"; //$NON-NLS-1$

    private static int searchRequestNum = 0;

    private Connection connection;

    private boolean useLdaps;

    private boolean useStartTLS;

    private String authMethod;

    private String bindPrincipal;

    private String bindCredentials;

    private String saslRealm;

    private Hashtable<String, String> environment;

    private InitialLdapContext context;

    private boolean isConnected;

    private Thread jobThread;

    private Collection<String> binaryAttributes;

    /** JNDI constant for "throw" referrals handling */
    public static final String REFERRAL_THROW = "throw"; //$NON-NLS-1$

    /** JNDI constant for "follow" referrals handling */
    public static final String REFERRAL_FOLLOW = "follow"; //$NON-NLS-1$

    /** JNDI constant for "ignore" referrals handling */
    public static final String REFERRAL_IGNORE = "ignore"; //$NON-NLS-1$

    /** JNDI constant for "searching" alias dereferencing */
    public static final String ALIAS_SEARCHING = "searching"; //$NON-NLS-1$

    /** JNDI constant for "finding" alias dereferencing */
    public static final String ALIAS_FINDING = "finding"; //$NON-NLS-1$

    /** JNDI constant for "always" alias dereferencing */
    public static final String ALIAS_ALWAYS = "always"; //$NON-NLS-1$

    /** JNDI constant for "never" alias dereferencing */
    public static final String ALIAS_NEVER = "never"; //$NON-NLS-1$

    /**
     * Creates a new instance of JNDIConnectionContext.
     * 
     * @param connection the connection
     */
    public JNDIConnectionWrapper(Connection connection) {
        this.connection = connection;
    }

    /**
     * {@inheritDoc}
     */
    public void connect(StudioProgressMonitor monitor) {
        context = null;
        isConnected = false;
        jobThread = null;

        try {
            doConnect(monitor);
        } catch (NamingException ne) {
            disconnect();
            monitor.reportError(ne);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void disconnect() {
        if (jobThread != null) {
            Thread t = jobThread;
            jobThread = null;
            t.interrupt();
        }
        if (context != null) {
            try {
                context.close();
            } catch (NamingException e) {
                // ignore
            }
            context = null;
        }
        isConnected = false;
    }

    /**
     * {@inheritDoc}
     */
    public void bind(StudioProgressMonitor monitor) {
        try {
            doBind(monitor);
        } catch (NamingException ne) {
            disconnect();
            monitor.reportError(ne);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void unbind() {
        disconnect();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isConnected() {
        return context != null;
    }

    /**
     * Sets the binary attributes.
     * 
     * @param binaryAttributes the binary attributes
     */
    public void setBinaryAttributes(Collection<String> binaryAttributes) {
        this.binaryAttributes = binaryAttributes;
        StringBuilder sb = new StringBuilder();

        for (String string : binaryAttributes) {
            sb.append(string).append(' ');
        }

        String binaryAttributesString = sb.toString();

        if (environment != null) {
            environment.put(JAVA_NAMING_LDAP_ATTRIBUTES_BINARY, binaryAttributesString);
        }

        if (context != null) {
            try {
                context.addToEnvironment(JAVA_NAMING_LDAP_ATTRIBUTES_BINARY, binaryAttributesString);
            } catch (NamingException e) {
                // TODO: logging
                e.printStackTrace();
            }
        }
    }

    /**
     * Search.
     * 
     * @param searchBase the search base
     * @param filter the filter
     * @param searchControls the controls
     * @param aliasesDereferencingMethod the aliases dereferencing method
     * @param referralsHandlingMethod the referrals handling method
     * @param controls the LDAP controls
     * @param monitor the progress monitor
     * @param referralsInfo the referrals info
     * 
     * @return the naming enumeration or null if an exception occurs.
     */
    public JndiStudioNamingEnumeration search(final String searchBase, final String filter,
            final SearchControls searchControls, final AliasDereferencingMethod aliasesDereferencingMethod,
            final ReferralHandlingMethod referralsHandlingMethod, final Control[] controls,
            final StudioProgressMonitor monitor, final ReferralsInfo referralsInfo) {
        final long requestNum = searchRequestNum++;

        // start
        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                LdapContext searchCtx = context;
                try {
                    // create the search context
                    searchCtx = context.newInstance(controls);

                    // translate alias dereferencing method
                    searchCtx.addToEnvironment(JAVA_NAMING_LDAP_DEREF_ALIASES,
                            translateDerefAliasMethod(aliasesDereferencingMethod));

                    // use "throw" as we handle referrals manually
                    searchCtx.addToEnvironment(Context.REFERRAL, REFERRAL_THROW);

                    // perform the search
                    NamingEnumeration<SearchResult> result = searchCtx
                            .search(JNDIConnectionWrapper.getSaveJndiName(searchBase), filter, searchControls);
                    namingEnumeration = new JndiStudioNamingEnumeration(connection, searchCtx, result, null,
                            searchBase, filter, searchControls, aliasesDereferencingMethod, referralsHandlingMethod,
                            controls, requestNum, monitor, referralsInfo);
                } catch (PartialResultException | ReferralException e) {
                    namingEnumeration = new JndiStudioNamingEnumeration(connection, searchCtx, null, e, searchBase,
                            filter, searchControls, aliasesDereferencingMethod, referralsHandlingMethod, controls,
                            requestNum, monitor, referralsInfo);
                } catch (NamingException e) {
                    namingException = e;
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    if (namingEnumeration != null) {
                        logger.logSearchRequest(connection, searchBase, filter, searchControls,
                                aliasesDereferencingMethod, controls, requestNum, namingException);
                    } else {
                        logger.logSearchRequest(connection, searchBase, filter, searchControls,
                                aliasesDereferencingMethod, controls, requestNum, namingException);
                        logger.logSearchResultDone(connection, 0, requestNum, namingException);
                    }
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (NamingException ne) {
            monitor.reportError(ne);
            return null;
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
            return null;
        } else {
            return runnable.getResult();
        }
    }

    /**
     * Modifies attributes of an entry.
     * 
     * @param dn the Dn
     * @param modificationItems the modification items
     * @param controls the controls
     * @param monitor the progress monitor
     * @param referralsInfo the referrals info
     */
    public void modifyEntry(final String dn, final ModificationItem[] modificationItems, final Control[] controls,
            final StudioProgressMonitor monitor, final ReferralsInfo referralsInfo) {
        if (connection.isReadOnly()) {
            monitor.reportError(
                    new Exception(NLS.bind(Messages.error__connection_is_readonly, connection.getName())));
            return;
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // create modify context
                    LdapContext modCtx = context.newInstance(controls);

                    // use "throw" as we handle referrals manually
                    modCtx.addToEnvironment(Context.REFERRAL, REFERRAL_THROW);

                    // perform modification
                    modCtx.modifyAttributes(getSaveJndiName(dn), modificationItems);
                } catch (ReferralException re) {
                    try {
                        ReferralsInfo newReferralsInfo = handleReferralException(re, referralsInfo);
                        Referral referral = newReferralsInfo.getNextReferral();

                        if (referral != null) {
                            Connection referralConnection = ConnectionWrapperUtils.getReferralConnection(referral,
                                    monitor, this);
                            if (referralConnection != null) {
                                List<String> urls = new ArrayList<>(referral.getLdapUrls());

                                String referralDn = new LdapUrl(urls.get(0)).getDn().getName();
                                referralConnection.getConnectionWrapper().modifyEntry(referralDn, modificationItems,
                                        controls, monitor, newReferralsInfo);
                            } else {
                                canceled = true;
                            }
                        }

                        return;
                    } catch (NamingException ne) {
                        namingException = ne;
                    } catch (LdapURLEncodingException e) {
                        namingException = new NamingException(e.getMessage());
                    }
                } catch (NamingException ne) {
                    namingException = ne;
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    logger.logChangetypeModify(connection, dn, modificationItems, controls, namingException);
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (NamingException ne) {
            monitor.reportError(ne);
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
        }
    }

    /**
     * Renames an entry.
     * 
     * @param oldDn the old Dn
     * @param newDn the new Dn
     * @param deleteOldRdn true to delete the old Rdn
     * @param controls the controls
     * @param monitor the progress monitor
     * @param referralsInfo the referrals info
     */
    public void renameEntry(final String oldDn, final String newDn, final boolean deleteOldRdn,
            final Control[] controls, final StudioProgressMonitor monitor, final ReferralsInfo referralsInfo) {
        if (connection.isReadOnly()) {
            monitor.reportError(
                    new Exception(NLS.bind(Messages.error__connection_is_readonly, connection.getName())));
            return;
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // create modify context
                    LdapContext modCtx = context.newInstance(controls);

                    // use "throw" as we handle referrals manually
                    modCtx.addToEnvironment(Context.REFERRAL, REFERRAL_THROW);

                    // delete old Rdn
                    if (deleteOldRdn) {
                        modCtx.addToEnvironment(JAVA_NAMING_LDAP_DELETE_RDN, "true"); //$NON-NLS-1$
                    } else {
                        modCtx.addToEnvironment(JAVA_NAMING_LDAP_DELETE_RDN, "false"); //$NON-NLS-1$
                    }

                    // rename entry
                    modCtx.rename(getSaveJndiName(oldDn), getSaveJndiName(newDn));
                } catch (ReferralException re) {
                    try {
                        ReferralsInfo newReferralsInfo = handleReferralException(re, referralsInfo);
                        Referral referral = newReferralsInfo.getNextReferral();
                        if (referral != null) {
                            Connection referralConnection = ConnectionWrapperUtils.getReferralConnection(referral,
                                    monitor, this);
                            if (referralConnection != null) {
                                referralConnection.getConnectionWrapper().renameEntry(oldDn, newDn, deleteOldRdn,
                                        controls, monitor, newReferralsInfo);
                            } else {
                                canceled = true;
                            }
                        }
                    } catch (NamingException ne) {
                        namingException = ne;
                    }
                } catch (NamingException ne) {
                    namingException = ne;
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    logger.logChangetypeModDn(connection, oldDn, newDn, deleteOldRdn, controls, namingException);
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (NamingException ne) {
            monitor.reportError(ne);
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
        }
    }

    /**
     * Creates an entry.
     * 
     * @param dn the entry's Dn
     * @param attributes the entry's attributes
     * @param controls the controls
     * @param monitor the progress monitor
     * @param referralsInfo the referrals info
     */
    public void createEntry(final String dn, final Attributes attributes, final Control[] controls,
            final StudioProgressMonitor monitor, final ReferralsInfo referralsInfo) {
        if (connection.isReadOnly()) {
            monitor.reportError(
                    new Exception(NLS.bind(Messages.error__connection_is_readonly, connection.getName())));
            return;
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // create modify context
                    LdapContext modCtx = context.newInstance(controls);

                    // use "throw" as we handle referrals manually
                    modCtx.addToEnvironment(Context.REFERRAL, REFERRAL_THROW);

                    // create entry
                    modCtx.createSubcontext(getSaveJndiName(dn), attributes);
                } catch (ReferralException re) {
                    try {
                        ReferralsInfo newReferralsInfo = handleReferralException(re, referralsInfo);
                        Referral referral = newReferralsInfo.getNextReferral();

                        if (referral != null) {
                            Connection referralConnection = ConnectionWrapperUtils.getReferralConnection(referral,
                                    monitor, this);

                            if (referralConnection != null) {
                                List<String> urls = new ArrayList<>(referral.getLdapUrls());

                                String referralDn = new LdapUrl(urls.get(0)).getDn().getName();
                                referralConnection.getConnectionWrapper().createEntry(referralDn, attributes,
                                        controls, monitor, newReferralsInfo);
                            } else {
                                canceled = true;
                            }
                        }
                    } catch (NamingException ne) {
                        namingException = ne;
                    } catch (LdapURLEncodingException e) {
                        namingException = new NamingException(e.getMessage());
                    }
                } catch (NamingException ne) {
                    namingException = ne;
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    logger.logChangetypeAdd(connection, dn, attributes, controls, namingException);
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (NamingException ne) {
            monitor.reportError(ne);
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
        }
    }

    /**
     * Deletes an entry.
     * 
     * @param dn the Dn of the entry to delete
     * @param controls the controls
     * @param monitor the progress monitor
     * @param referralsInfo the referrals info
     */
    public void deleteEntry(final String dn, final Control[] controls, final StudioProgressMonitor monitor,
            final ReferralsInfo referralsInfo) {
        if (connection.isReadOnly()) {
            monitor.reportError(
                    new Exception(NLS.bind(Messages.error__connection_is_readonly, connection.getName())));
            return;
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // create modify context
                    LdapContext modCtx = context.newInstance(controls);

                    // use "throw" as we handle referrals manually
                    modCtx.addToEnvironment(Context.REFERRAL, REFERRAL_THROW);

                    // delete entry
                    modCtx.destroySubcontext(getSaveJndiName(dn));
                } catch (ReferralException re) {
                    try {
                        ReferralsInfo newReferralsInfo = handleReferralException(re, referralsInfo);
                        Referral referral = newReferralsInfo.getNextReferral();
                        if (referral != null) {
                            Connection referralConnection = ConnectionWrapperUtils.getReferralConnection(referral,
                                    monitor, this);
                            if (referralConnection != null) {
                                List<String> urls = new ArrayList<>(referral.getLdapUrls());

                                String referralDn = new LdapUrl(urls.get(0)).getDn().getName();
                                referralConnection.getConnectionWrapper().deleteEntry(referralDn, controls, monitor,
                                        newReferralsInfo);
                            } else {
                                canceled = true;
                            }
                        }
                    } catch (NamingException ne) {
                        namingException = ne;
                    } catch (LdapURLEncodingException e) {
                        namingException = new NamingException(e.getMessage());
                    }
                } catch (NamingException ne) {
                    namingException = ne;
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    logger.logChangetypeDelete(connection, dn, controls, namingException);
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (NamingException ne) {
            monitor.reportError(ne);
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
        }
    }

    private void doConnect(final StudioProgressMonitor monitor) throws NamingException {
        context = null;
        isConnected = true;

        // setup connection parameters
        String host = connection.getConnectionParameter().getHost();
        int port = connection.getConnectionParameter().getPort();
        long timeout = connection.getConnectionParameter().getTimeout();

        useLdaps = connection.getConnectionParameter()
                .getEncryptionMethod() == ConnectionParameter.EncryptionMethod.LDAPS;
        useStartTLS = connection.getConnectionParameter()
                .getEncryptionMethod() == ConnectionParameter.EncryptionMethod.START_TLS;

        environment = new Hashtable<>();
        Preferences preferences = ConnectionCorePlugin.getDefault().getPluginPreferences();
        final boolean validateCertificates = preferences
                .getBoolean(ConnectionCoreConstants.PREFERENCE_VALIDATE_CERTIFICATES);
        String ldapCtxFactory = preferences.getString(ConnectionCoreConstants.PREFERENCE_LDAP_CONTEXT_FACTORY);
        environment.put(Context.INITIAL_CONTEXT_FACTORY, ldapCtxFactory);
        environment.put(JAVA_NAMING_LDAP_VERSION, "3"); //$NON-NLS-1$

        // timeouts
        /*
         *  Don't use a timeout when using ldaps: JNDI throws a SocketException  when setting a timeout on SSL connections.
         *  See https://bugs.openjdk.java.net/browse/JDK-8173451
         */
        if (!useLdaps) {
            if (timeout < 0) {
                timeout = 0;
            }
            environment.put(COM_SUN_JNDI_LDAP_CONNECT_TIMEOUT, Long.toString(timeout)); //$NON-NLS-1$
        }

        environment.put(COM_SUN_JNDI_DNS_TIMEOUT_INITIAL, "2000"); //$NON-NLS-1$
        environment.put(COM_SUN_JNDI_DNS_TIMEOUT_RETRIES, "3"); //$NON-NLS-1$

        // ldaps://
        if (useLdaps) {
            environment.put(Context.PROVIDER_URL, LdapUrl.LDAPS_SCHEME + host + ':' + port);
            environment.put(Context.SECURITY_PROTOCOL, "ssl"); //$NON-NLS-1$
            // host name verification is done in StudioTrustManager
            environment.put(JAVA_NAMING_LDAP_FACTORY_SOCKET,
                    validateCertificates ? StudioSSLSocketFactory.class.getName()
                            : DummySSLSocketFactory.class.getName());
        } else {
            environment.put(Context.PROVIDER_URL, LdapUrl.LDAP_SCHEME + host + ':' + port);
        }

        if (binaryAttributes != null) {
            setBinaryAttributes(binaryAttributes);
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    context = new InitialLdapContext(environment, null);

                    if (useStartTLS) {
                        try {
                            StartTlsResponse tls = (StartTlsResponse) context
                                    .extendedOperation(new StartTlsRequest());
                            // deactivate host name verification at this level,
                            // host name verification is done in StudioTrustManager
                            tls.setHostnameVerifier((hostname, session) -> true);

                            if (validateCertificates) {
                                tls.negotiate(StudioSSLSocketFactory.getDefault());
                            } else {
                                tls.negotiate(DummySSLSocketFactory.getDefault());
                            }
                        } catch (Exception e) {
                            namingException = new NamingException(e.getMessage() != null ? e.getMessage()
                                    : "Error while establishing TLS session"); //$NON-NLS-1$
                            namingException.setRootCause(e);
                            context.close();
                        }
                    }
                } catch (NamingException ne) {
                    namingException = ne;
                }
            }
        };

        runAndMonitor(runnable, monitor);

        if (runnable.getException() != null) {
            throw runnable.getException();
        } else if (context != null) {
            // all OK
        } else {
            throw new NamingException("???"); //$NON-NLS-1$
        }
    }

    private void doBind(final StudioProgressMonitor monitor) throws NamingException {
        if (context != null && isConnected) {
            // setup authentication methdod
            authMethod = AUTHMETHOD_NONE;
            if (connection.getConnectionParameter()
                    .getAuthMethod() == ConnectionParameter.AuthenticationMethod.SIMPLE) {
                authMethod = AUTHMETHOD_SIMPLE;
            } else if (connection.getConnectionParameter()
                    .getAuthMethod() == ConnectionParameter.AuthenticationMethod.SASL_DIGEST_MD5) {
                authMethod = AUTHMETHOD_DIGEST_MD5;
                saslRealm = connection.getConnectionParameter().getSaslRealm();
            } else if (connection.getConnectionParameter()
                    .getAuthMethod() == ConnectionParameter.AuthenticationMethod.SASL_CRAM_MD5) {
                authMethod = AUTHMETHOD_CRAM_MD5;
            } else if (connection.getConnectionParameter()
                    .getAuthMethod() == ConnectionParameter.AuthenticationMethod.SASL_GSSAPI) {
                authMethod = AUTHMETHOD_GSSAPI;
            }

            // No Authentication
            if (authMethod == AUTHMETHOD_NONE) {
                bindPrincipal = ""; //$NON-NLS-1$
                bindCredentials = ""; //$NON-NLS-1$
            } else {
                // setup credentials
                IAuthHandler authHandler = ConnectionCorePlugin.getDefault().getAuthHandler();
                if (authHandler == null) {
                    NamingException namingException = new NamingException(Messages.model__no_auth_handler);
                    monitor.reportError(Messages.model__no_auth_handler, namingException);
                    throw namingException;
                }
                ICredentials credentials = authHandler.getCredentials(connection.getConnectionParameter());
                if (credentials == null) {
                    CancelException cancelException = new CancelException();
                    monitor.setCanceled(true);
                    monitor.reportError(Messages.model__no_credentials, cancelException);
                    throw cancelException;
                }
                if (credentials.getBindPrincipal() == null || credentials.getBindPassword() == null) {
                    NamingException namingException = new NamingException(Messages.model__no_credentials);
                    monitor.reportError(Messages.model__no_credentials, namingException);
                    throw namingException;
                }
                bindPrincipal = credentials.getBindPrincipal();
                bindCredentials = credentials.getBindPassword();
            }

            InnerRunnable runnable = new InnerRunnable() {
                public void run() {
                    try {
                        context.removeFromEnvironment(Context.SECURITY_AUTHENTICATION);
                        context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
                        context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
                        context.removeFromEnvironment(JAVA_NAMING_SECURITY_SASL_REALM);

                        context.addToEnvironment(Context.SECURITY_AUTHENTICATION, authMethod);

                        // SASL options
                        if (connection.getConnectionParameter()
                                .getAuthMethod() == AuthenticationMethod.SASL_CRAM_MD5
                                || connection.getConnectionParameter()
                                        .getAuthMethod() == AuthenticationMethod.SASL_DIGEST_MD5
                                || connection.getConnectionParameter()
                                        .getAuthMethod() == AuthenticationMethod.SASL_GSSAPI) {
                            // Request quality of protection
                            switch (connection.getConnectionParameter().getSaslQop()) {
                            case AUTH:
                                context.addToEnvironment(Sasl.QOP, SaslQoP.AUTH.getValue());
                                break;
                            case AUTH_INT:
                                context.addToEnvironment(Sasl.QOP, SaslQoP.AUTH_INT.getValue());
                                break;
                            case AUTH_CONF:
                                context.addToEnvironment(Sasl.QOP, SaslQoP.AUTH_CONF.getValue());
                                break;
                            }

                            // Request mutual authentication
                            if (connection.getConnectionParameter().isSaslMutualAuthentication()) {
                                context.addToEnvironment(Sasl.SERVER_AUTH, "true"); //$NON-NLS-1$
                            } else {
                                context.removeFromEnvironment(Sasl.SERVER_AUTH);
                            }

                            // Request cryptographic protection strength
                            switch (connection.getConnectionParameter().getSaslSecurityStrength()) {
                            case HIGH:
                                context.addToEnvironment(Sasl.STRENGTH, SaslSecurityStrength.HIGH.getValue());
                                break;
                            case MEDIUM:
                                context.addToEnvironment(Sasl.STRENGTH, SaslSecurityStrength.MEDIUM.getValue());
                                break;
                            case LOW:
                                context.addToEnvironment(Sasl.STRENGTH, SaslSecurityStrength.LOW.getValue());
                                break;
                            }
                        }

                        // Bind
                        if (connection.getConnectionParameter()
                                .getAuthMethod() == ConnectionParameter.AuthenticationMethod.SASL_GSSAPI) {
                            // GSSAPI
                            doGssapiBind(this);
                        } else {
                            // no GSSAPI
                            context.addToEnvironment(Context.SECURITY_PRINCIPAL, bindPrincipal);
                            context.addToEnvironment(Context.SECURITY_CREDENTIALS, bindCredentials);

                            if (connection.getConnectionParameter()
                                    .getAuthMethod() == ConnectionParameter.AuthenticationMethod.SASL_DIGEST_MD5
                                    && StringUtils.isNotEmpty(saslRealm)) {
                                context.addToEnvironment(JAVA_NAMING_SECURITY_SASL_REALM, saslRealm);
                            }

                            context.reconnect(context.getConnectControls());
                        }
                    } catch (NamingException ne) {
                        namingException = ne;
                    }
                }
            };

            runAndMonitor(runnable, monitor);

            if (runnable.getException() != null) {
                throw runnable.getException();
            } else if (context != null) {
                // all OK
            } else {
                throw new NamingException("???"); //$NON-NLS-1$
            }
        } else {
            throw new NamingException(NO_CONNECTION);
        }
    }

    private void doGssapiBind(final InnerRunnable innerRunnable) throws NamingException {
        File configFile = null;
        try {
            Preferences preferences = ConnectionCorePlugin.getDefault().getPluginPreferences();
            boolean useKrb5SystemProperties = preferences
                    .getBoolean(ConnectionCoreConstants.PREFERENCE_USE_KRB5_SYSTEM_PROPERTIES);
            String krb5LoginModule = preferences.getString(ConnectionCoreConstants.PREFERENCE_KRB5_LOGIN_MODULE);

            if (!useKrb5SystemProperties) {
                // Kerberos Configuration
                switch (connection.getConnectionParameter().getKrb5Configuration()) {
                case DEFAULT:
                    // nothing 
                    System.clearProperty("java.security.krb5.conf"); //$NON-NLS-1$
                    break;
                case FILE:
                    // use specified krb5.conf
                    System.setProperty("java.security.krb5.conf", connection.getConnectionParameter() //$NON-NLS-1$
                            .getKrb5ConfigurationFile());
                    break;
                case MANUAL:
                    // write manual config parameters to connection specific krb5.conf file
                    String fileName = Utils.getFilenameString(connection.getId()) + ".krb5.conf"; //$NON-NLS-1$
                    configFile = ConnectionCorePlugin.getDefault().getStateLocation().append(fileName).toFile();
                    String realm = connection.getConnectionParameter().getKrb5Realm();
                    String host = connection.getConnectionParameter().getKrb5KdcHost();
                    int port = connection.getConnectionParameter().getKrb5KdcPort();
                    StringBuilder sb = new StringBuilder();
                    sb.append("[libdefaults]").append(ConnectionCoreConstants.LINE_SEPARATOR); //$NON-NLS-1$
                    sb.append("default_realm = ").append(realm).append(ConnectionCoreConstants.LINE_SEPARATOR); //$NON-NLS-1$
                    sb.append("[realms]").append(ConnectionCoreConstants.LINE_SEPARATOR); //$NON-NLS-1$
                    sb.append(realm).append(" = {").append(ConnectionCoreConstants.LINE_SEPARATOR); //$NON-NLS-1$
                    sb.append("kdc = ").append(host).append(":").append(port).append( //$NON-NLS-1$ //$NON-NLS-2$
                            ConnectionCoreConstants.LINE_SEPARATOR);
                    sb.append("}").append(ConnectionCoreConstants.LINE_SEPARATOR); //$NON-NLS-1$
                    try {
                        FileUtils.writeStringToFile(configFile, sb.toString());
                    } catch (IOException ioe) {
                        NamingException ne = new NamingException();
                        ne.setRootCause(ioe);
                        throw ne;
                    }
                    System.setProperty("java.security.krb5.conf", configFile.getAbsolutePath()); //$NON-NLS-1$
                }

                // Use our custom configuration so we don't need to mess with external configuration
                Configuration.setConfiguration(new InnerConfiguration(krb5LoginModule));
            }

            // Gets the TGT, either from native ticket cache or obtain new from KDC
            LoginContext lc = null;
            try {
                lc = new LoginContext(this.getClass().getName(), new InnerCallbackHandler());
                lc.login();
            } catch (LoginException le) {
                NamingException ne = new NamingException();
                ne.setRootCause(le);
                throw ne;
            }

            // Login to LDAP server, obtains a service ticket from KDC
            Subject.doAs(lc.getSubject(), (PrivilegedAction<Object>) () -> {
                try {
                    context.reconnect(context.getConnectControls());
                } catch (NamingException ne) {
                    innerRunnable.namingException = ne;
                }
                return null;
            });
        } finally {
            // delete temporary config file
            if (configFile != null && configFile.exists()) {
                configFile.delete();
            }
        }
    }

    private void checkConnectionAndRunAndMonitor(final InnerRunnable runnable, final StudioProgressMonitor monitor)
            throws NamingException {
        // check connection
        if (!isConnected || context == null) {
            doConnect(monitor);
            doBind(monitor);
        }
        if (context == null) {
            throw new NamingException(NO_CONNECTION);
        }

        // loop for reconnection
        for (int i = 0; i <= 1; i++) {
            runAndMonitor(runnable, monitor);

            // check reconnection
            if (i == 0 && ((runnable.getException() instanceof CommunicationException)
                    || (runnable.getException() instanceof ServiceUnavailableException)
                    || (runnable.getException() instanceof InsufficientResourcesException))) {

                doConnect(monitor);
                doBind(monitor);
                runnable.reset();
            } else {
                break;
            }
        }
    }

    private void runAndMonitor(final InnerRunnable runnable, final StudioProgressMonitor monitor)
            throws CancelException {
        if (!monitor.isCanceled()) {
            // monitor
            StudioProgressMonitor.CancelListener listener = event -> {
                if (monitor.isCanceled()) {
                    if (jobThread != null && jobThread.isAlive()) {
                        jobThread.interrupt();
                    }

                    if (context != null) {
                        try {
                            context.close();
                        } catch (NamingException ne) {
                        }

                        isConnected = false;
                        context = null;
                    }

                    isConnected = false;
                }
            };

            monitor.addCancelListener(listener);
            jobThread = Thread.currentThread();

            // run
            try {
                runnable.run();
            } finally {
                monitor.removeCancelListener(listener);
                jobThread = null;
            }

            if (monitor.isCanceled()) {
                throw new CancelException();
            }
        }
    }

    private final class InnerConfiguration extends Configuration {
        private String krb5LoginModule;
        private AppConfigurationEntry[] configList = null;

        public InnerConfiguration(String krb5LoginModule) {
            this.krb5LoginModule = krb5LoginModule;
        }

        public AppConfigurationEntry[] getAppConfigurationEntry(String applicationName) {
            if (configList == null) {
                HashMap<String, Object> options = new HashMap<>();

                // TODO: this only works for Sun JVM
                options.put("refreshKrb5Config", "true"); //$NON-NLS-1$ //$NON-NLS-2$

                switch (connection.getConnectionParameter().getKrb5CredentialConfiguration()) {
                case USE_NATIVE:
                    options.put("useTicketCache", "true"); //$NON-NLS-1$ //$NON-NLS-2$
                    options.put("doNotPrompt", "true"); //$NON-NLS-1$ //$NON-NLS-2$
                    break;

                case OBTAIN_TGT:
                    options.put("doNotPrompt", "false"); //$NON-NLS-1$ //$NON-NLS-2$
                    break;
                }

                configList = new AppConfigurationEntry[1];
                configList[0] = new AppConfigurationEntry(krb5LoginModule, LoginModuleControlFlag.REQUIRED,
                        options);
            }
            return configList;
        }

        @Override
        public void refresh() {
        }
    }

    private final class InnerCallbackHandler implements CallbackHandler {
        public void handle(Callback[] callbacks) throws UnsupportedCallbackException, IOException {
            for (int ii = 0; ii < callbacks.length; ii++) {
                Callback callBack = callbacks[ii];

                if (callBack instanceof NameCallback) {
                    // Handles username callback.
                    NameCallback nameCallback = (NameCallback) callBack;
                    nameCallback.setName(bindPrincipal);
                } else if (callBack instanceof PasswordCallback) {
                    // Handles password callback.
                    PasswordCallback passwordCallback = (PasswordCallback) callBack;
                    passwordCallback.setPassword(bindCredentials.toCharArray());
                } else {
                    throw new UnsupportedCallbackException(callBack, "Callback not supported"); //$NON-NLS-1$
                }
            }
        }
    }

    abstract class InnerRunnable implements Runnable {
        protected JndiStudioNamingEnumeration namingEnumeration = null;
        protected NamingException namingException = null;
        protected boolean canceled = false;

        /**
         * Gets the exception.
         * 
         * @return the exception
         */
        public NamingException getException() {
            return namingException;
        }

        /**
         * Gets the result.
         * 
         * @return the result
         */
        public JndiStudioNamingEnumeration getResult() {
            return namingEnumeration;
        }

        /**
         * Checks if is canceled.
         * 
         * @return true, if is canceled
         */
        public boolean isCanceled() {
            return canceled;
        }

        /**
         * Reset.
         */
        public void reset() {
            namingEnumeration = null;
            namingException = null;
            canceled = false;
        }
    }

    private List<IJndiLogger> getJndiLoggers() {
        return ConnectionCorePlugin.getDefault().getJndiLoggers();
    }

    /**
     * Translates the alias dereferencing method to its JNDI specific string.
     * 
     * @param aliasDereferencingMethod the alias dereferencing method
     * 
     * @return the JNDI specific alias dereferencing method string
     */
    private String translateDerefAliasMethod(AliasDereferencingMethod aliasDereferencingMethod) {
        String m = ALIAS_ALWAYS;

        switch (aliasDereferencingMethod) {
        case NEVER:
            m = ALIAS_NEVER;
            break;
        case ALWAYS:
            m = ALIAS_ALWAYS;
            break;
        case FINDING:
            m = ALIAS_FINDING;
            break;
        case SEARCH:
            m = ALIAS_SEARCHING;
            break;
        }

        return m;
    }

    /**
     * Gets a Name object that is save for JNDI operations.
     * <p>
     * In JNDI we have could use the following classes for names:
     * <ul>
     * <li>Dn as String</li>
     * <li>javax.naming.CompositeName</li>
     * <li>javax.naming.ldap.LdapName (since Java5)</li>
     * <li>org.apache.directory.api.ldap.name.LdapDN</li>
     * </ul>
     * <p>
     * There are some drawbacks when using this classes:
     * <ul>
     * <li>When passing Dn as String, JNDI doesn't handle slashes '/' correctly.
     * So we must use a Name object here.</li>
     * <li>With CompositeName we have the same problem with slashes '/'.</li>
     * <li>When using LdapDN from shared-ldap, JNDI uses the toString() method
     * and LdapDN.toString() returns the normalized ATAV, but we need the
     * user provided ATAV.</li>
     * <li>When using LdapName for the empty Dn (Root DSE) JNDI _sometimes_ throws
     * an Exception (java.lang.IndexOutOfBoundsException: Posn: -1, Size: 0
     * at javax.naming.ldap.LdapName.getPrefix(LdapName.java:240)).</li>
     * <li>Using LdapDN for the RootDSE doesn't work with Apache Harmony because
     * its JNDI provider only accepts intstances of CompositeName or LdapName.</li>
     * </ul>
     * <p>
     * So we use LdapName as default and the CompositeName for the empty Dn.
     * 
     * @param name the Dn
     * 
     * @return the save JNDI name
     * 
     * @throws InvalidNameException the invalid name exception
     */
    static Name getSaveJndiName(String name) throws InvalidNameException {
        if (name == null || StringUtils.isEmpty(name)) //$NON-NLS-1$
        {
            return new CompositeName();
        } else {
            return new LdapName(name);
        }
    }

    /**
     * Retrieves all referrals from the ReferralException and
     * creates or updates the ReferralsInfo.
     * 
     * @param referralException the referral exception
     * @param initialReferralsInfo the initial referrals info, may be null
     * 
     * @return the created or updated referrals info
     * 
     * @throws NamingException if a loop was encountered.
     */
    static ReferralsInfo handleReferralException(ReferralException referralException,
            ReferralsInfo initialReferralsInfo) throws NamingException {
        if (initialReferralsInfo == null) {
            initialReferralsInfo = new ReferralsInfo(true);
        }

        Referral referral = handleReferralException(referralException, initialReferralsInfo, null);

        while (referralException.skipReferral()) {
            try {
                Context ctx = referralException.getReferralContext();
                ctx.list(StringUtils.EMPTY); //$NON-NLS-1$
            } catch (NamingException ne) {
                if (ne instanceof ReferralException) {
                    if (ne != referralException) {
                        // what a hack: 
                        // if the same exception is throw, we have another URL for the same Referral/SearchResultReference
                        // if another exception is throws, we have a new Referral/SearchResultReference
                        // in the latter case we null out the reference, a new one will be created by handleReferral()
                        referral = null;
                    }

                    referralException = (ReferralException) ne;

                    referral = handleReferralException(referralException, initialReferralsInfo, referral);
                } else {
                    break;
                }
            }
        }

        return initialReferralsInfo;
    }

    private static Referral handleReferralException(ReferralException referralException,
            ReferralsInfo initialReferralsInfo, Referral referral) {
        String info = (String) referralException.getReferralInfo();

        if (referral == null) {
            referral = new ReferralImpl();
            initialReferralsInfo.addReferral(referral);
        }

        referral.addLdapUrl(info);

        return referral;
    }

}