--- a/java/org/apache/catalina/realm/LdapTlsContextFactory.java +++ a/java/org/apache/catalina/realm/LdapTlsContextFactory.java @@ -0,0 +1,177 @@ +/* + * 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.catalina.realm; + +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.logging.Logger; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import javax.naming.ldap.StartTlsRequest; +import javax.naming.ldap.StartTlsResponse; +import javax.naming.spi.InitialContextFactory; +import javax.net.ssl.SSLSession; + +import org.apache.catalina.util.StringManager; + +/** + * Implements an {@link InitialContextFactory} which will create + * {@link LdapContext} instances which have tls enabled.
+ * + * Pooling will be disabled as recommended by sun. + * + * @author Felix Schumacher + * + */ +public class LdapTlsContextFactory implements InitialContextFactory { + + /** + * The string manager for this package. + */ + protected static StringManager sm = StringManager + .getManager(Constants.Package); + + /** + * Proxies a {@link LdapContext} and handles instantiation and close + * specially to start TLS and end it. + */ + private static final class ProxyLdapContext implements InvocationHandler { + private final LdapContext delegate; + private final StartTlsResponse tls; + + @SuppressWarnings("unchecked") + private ProxyLdapContext(Hashtable origEnv) throws NamingException { + Hashtable env = new Hashtable(origEnv); + /* + * We want to have login to happen after TLS was established, so + * save credentials for later usage and remove them from env. + */ + Map credentials = new HashMap(); + for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION, + Context.SECURITY_CREDENTIALS, Context.SECURITY_PRINCIPAL, + Context.SECURITY_PROTOCOL)) { + Object entry = env.remove(key); + if (entry != null) { + credentials.put(key, entry); + } + } + delegate = new InitialLdapContext(env, null); + tls = (StartTlsResponse) delegate + .extendedOperation(new StartTlsRequest()); + try { + SSLSession negotiate = tls.negotiate(); + Logger.getLogger(this.getClass().getCanonicalName()).fine( + sm.getString("LDAP connection protocol is {0}", + negotiate.getProtocol())); + } catch (IOException e) { + throw new NamingException(e.getMessage()); + } + /* + * now is the time to reinstate the credentials into env + */ + for (Map.Entry savedEntry : credentials.entrySet()) { + delegate.addToEnvironment(savedEntry.getKey(), savedEntry + .getValue()); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if ("close".equals(method.getName())) { + return doClose(delegate); + } + return method.invoke(delegate, args); + } + + /** + * Wrapper to the original close method. It will try to end tls before + * closing the underlying connection. + * + * @param delegate + * underlying connection + * @return always null + * @throws InvocationTargetException + * if an {@link IOException} or a {@link NamingException} + * was catched while closing the connection + */ + private Object doClose(LdapContext delegate) + throws InvocationTargetException { + try { + if (tls != null) { + try { + tls.close(); + } catch (IOException e) { + throw new InvocationTargetException(e); + } + } + } finally { + try { + if (delegate != null) { + delegate.close(); + } + } catch (NamingException e) { + throw new InvocationTargetException(e); + } + } + return null; + } + } + + /** + * Environment key under which the JNDI context factory is stored which + * shall be used inside the proxy. + */ + public static final String REAL_INITIAL_CONTEXT_FACTORY = "REAL_INITIAL_CONTEXT_FACTORY"; + + /** + * The JNDI context factory used to acquire our InitialContext. By default, + * assumes use of an LDAP server using the standard JNDI LDAP provider. + */ + private static final String DEFAULT_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; + + @SuppressWarnings("unchecked") + @Override + public Context getInitialContext(final Hashtable environment) + throws NamingException { + final Hashtable proxyEnv = new Hashtable(environment); + final Object realFactory; + if (environment.contains(REAL_INITIAL_CONTEXT_FACTORY)) { + realFactory = environment.get(REAL_INITIAL_CONTEXT_FACTORY); + } else { + realFactory = DEFAULT_CONTEXT_FACTORY; + } + proxyEnv.put(Context.INITIAL_CONTEXT_FACTORY, realFactory); + proxyEnv.put("com.sun.jndi.ldap.connect.pool", "false"); + return (Context) Proxy.newProxyInstance(this.getClass() + .getClassLoader(), new Class[] { DirContext.class }, + new ProxyLdapContext(proxyEnv)); + } + +} --- a/java/org/apache/catalina/realm/LocalStrings.properties +++ a/java/org/apache/catalina/realm/LocalStrings.properties @@ -98,3 +98,4 @@ combinedRealm.addRealm=Add "{0}" realm, making a total of "{1}" realms combinedRealm.realmStartFail=Failed to start "{0}" realm lockOutRealm.authLockedUser=An attempt was made to authenticate the locked user "{0}" lockOutRealm.removeWarning=User "{0}" was removed from the failed users cache after {1} seconds to keep the cache size within the limit set +tlsContextFactory.protocolInfo=connection protocol is {0}