--- old/src/java.base/share/classes/module-info.java 2017-11-29 17:13:34.108977588 +0000 +++ new/src/java.base/share/classes/module-info.java 2017-11-29 17:13:33.996977071 +0000 @@ -249,6 +249,7 @@ java.datatransfer, java.management, java.management.rmi, + java.naming, java.rmi, java.sql.rowset, java.xml; --- old/src/java.naming/share/classes/com/sun/jndi/ldap/LdapCtxFactory.java 2017-11-29 17:13:34.376978827 +0000 +++ new/src/java.naming/share/classes/com/sun/jndi/ldap/LdapCtxFactory.java 2017-11-29 17:13:34.264978310 +0000 @@ -25,12 +25,11 @@ package com.sun.jndi.ldap; -import java.util.Hashtable; -import java.util.Vector; -import java.util.Enumeration; +import java.util.*; import javax.naming.*; import javax.naming.directory.*; +import javax.naming.ldap.LdapDnsProviderResult; import javax.naming.spi.ObjectFactory; import javax.naming.spi.InitialContextFactory; import javax.naming.ldap.Control; @@ -158,41 +157,74 @@ } private static DirContext getUsingURL(String url, Hashtable env) - throws NamingException { - DirContext ctx = null; - LdapURL ldapUrl = new LdapURL(url); - String dn = ldapUrl.getDN(); - String host = ldapUrl.getHost(); - int port = ldapUrl.getPort(); - String[] hostports; - String domainName = null; - - // handle a URL with no hostport (ldap:/// or ldaps:///) - // locate the LDAP service using the URL's distinguished name - if (host == null && - port == -1 && - dn != null && - (domainName = ServiceLocator.mapDnToDomainName(dn)) != null && - (hostports = ServiceLocator.getLdapService(domainName, env)) - != null) { - // Generate new URLs that include the discovered hostports. - // Reuse the original URL scheme. - String scheme = ldapUrl.getScheme() + "://"; - String[] newUrls = new String[hostports.length]; - String query = ldapUrl.getQuery(); - String urlSuffix = ldapUrl.getPath() + (query != null ? query : ""); - for (int i = 0; i < hostports.length; i++) { - newUrls[i] = scheme + hostports[i] + urlSuffix; - } - ctx = getUsingURLs(newUrls, env); - // Associate the derived domain name with the context - ((LdapCtx)ctx).setDomainName(domainName); + throws NamingException + { + try { + LdapDnsProviderResult r = + LdapDnsProviderService.getInstance().lookupEndpoints(url, env); + LdapCtx ctx = null; + NamingException lastException = null; + + /* + * Prior to this change we had been assuming that the url.getDN() + * should be converted to a domain name via + * ServiceLocator.mapDnToDomainName(url.getDN()) + * + * However this is incorrect as we can't assume that the supplied + * url.getDN() is the same as the dns domain for the directory + * server. + * + * This means that we depend on the dnsProvider to return both + * the list of urls of individual hosts from which we attempt to + * create an LdapCtx from *AND* the domain name that they serve + * + * In order to do this the dnsProvider must return an + * {@link LdapDnsProviderResult}. + * + */ + for (String u : r.getEndpoints()) { + try { + ctx = getLdapCtxFromUrl( + r.getDomainName(), new LdapURL(u), env); + } catch (NamingException e) { + // try the next element + lastException = e; + } + } + + if (lastException != null) { + throw lastException; + } + + if (ctx == null) { + // we have resolved them, but they are not valid + throw new NamingException("Could not resolve a valid ldap host"); + } - } else { - ctx = new LdapCtx(dn, host, port, env, ldapUrl.useSsl()); // Record the URL that created the context - ((LdapCtx)ctx).setProviderUrl(url); + ctx.setProviderUrl(url); + return ctx; + } catch (NamingException e) { + // getDnsUrls(url, env) may throw a NamingException, which there is + // no need to wrap. + throw e; + } catch (Exception e) { + NamingException ex = new NamingException(); + ex.setRootCause(e); + throw ex; } + } + + private static LdapCtx getLdapCtxFromUrl(String domain, + LdapURL url, + Hashtable env) + throws NamingException + { + String dn = url.getDN(); + String host = url.getHost(); + int port = url.getPort(); + LdapCtx ctx = new LdapCtx(dn, host, port, env, url.useSsl()); + ctx.setDomainName(domain); return ctx; } @@ -202,19 +234,17 @@ * Not pretty, but potentially more informative than returning null. */ private static DirContext getUsingURLs(String[] urls, Hashtable env) - throws NamingException { - NamingException ne = null; - DirContext ctx = null; - for (int i = 0; i < urls.length; i++) { + throws NamingException + { + NamingException ex = null; + for (String u : urls) { try { - return getUsingURL(urls[i], env); - } catch (AuthenticationException e) { - throw e; + return getUsingURL(u, env); } catch (NamingException e) { - ne = e; + ex = e; } } - throw ne; + throw ex; } /** --- old/src/java.naming/share/classes/module-info.java 2017-11-29 17:13:34.620979955 +0000 +++ new/src/java.naming/share/classes/module-info.java 2017-11-29 17:13:34.512979455 +0000 @@ -46,6 +46,7 @@ uses javax.naming.ldap.StartTlsResponse; uses javax.naming.spi.InitialContextFactory; + uses javax.naming.ldap.LdapDnsProvider; provides java.security.Provider with sun.security.provider.certpath.ldap.JdkLDAP; --- /dev/null 2017-11-29 10:09:48.676000048 +0000 +++ new/src/java.naming/share/classes/com/sun/jndi/ldap/DefaultLdapDnsProvider.java 2017-11-29 17:13:34.752980564 +0000 @@ -0,0 +1,52 @@ +package com.sun.jndi.ldap; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import javax.naming.NamingException; +import javax.naming.ldap.LdapDnsProvider; +import javax.naming.ldap.LdapDnsProviderResult; + +public class DefaultLdapDnsProvider extends LdapDnsProvider { + @Override + public LdapDnsProviderResult lookupEndpoints(String url, Hashtable env) { + String domainName = ""; + List urls = new ArrayList<>(); + try { + LdapURL ldapUrl = new LdapURL(url); + String dn = ldapUrl.getDN(); + String host = ldapUrl.getHost(); + int port = ldapUrl.getPort(); + String[] hostports; + + // handle a URL with no hostport (ldap:/// or ldaps:///) + // locate the LDAP service using the URL's distinguished name + if (host == null + && port == -1 + && dn != null + && (domainName = ServiceLocator.mapDnToDomainName(dn)) != null + && (hostports = ServiceLocator.getLdapService(domainName, env)) != null) + { + // Generate new URLs that include the discovered hostports. + // Reuse the original URL scheme. + String scheme = ldapUrl.getScheme() + "://"; + String query = ldapUrl.getQuery(); + String urlSuffix = ldapUrl.getPath() + (query != null ? query : ""); + for (int i = 0; i < hostports.length; i++) { + // the hostports come from the DNS SRV records + // we assume the SRV record is scheme aware + urls.add(scheme + hostports[i] + urlSuffix); + } + } else { + // we don't have enough information to set the domain name + // correctly + domainName = ""; + urls.add(url); + } + } catch (NamingException e) { + // leave list of resolved urls empty + } + return new LdapDnsProviderResult(domainName, urls); + } + +} --- /dev/null 2017-11-29 10:09:48.676000048 +0000 +++ new/src/java.naming/share/classes/com/sun/jndi/ldap/LdapDnsProviderService.java 2017-11-29 17:13:35.004981729 +0000 @@ -0,0 +1,88 @@ +package com.sun.jndi.ldap; + +import com.sun.jndi.ldap.DefaultLdapDnsProvider; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import javax.naming.ldap.LdapDnsProvider; +import javax.naming.ldap.LdapDnsProviderResult; +import sun.security.util.SecurityConstants; + +/** + * The {@code LdapDnsProviderService} is responsible for creating and providing + * access to the registered {@code LdapDnsProvider}s. The ServiceLoader is used + * to find and register any implementations of {@link LdapDnsProvider}. + */ +class LdapDnsProviderService { + + private static LdapDnsProviderService service; + private static final Object lock = new int[0]; + private final ServiceLoader providers; + + /** + * Creates a new instance of LdapDnsProviderService + */ + private LdapDnsProviderService() { + SecurityManager sm = System.getSecurityManager(); + if (sm == null) { + providers = ServiceLoader.load( + LdapDnsProvider.class, + ClassLoader.getSystemClassLoader()); + } else { + final PrivilegedAction> pa = + () -> ServiceLoader.load( + LdapDnsProvider.class, + ClassLoader.getSystemClassLoader()); + + providers = AccessController.doPrivileged( + pa, + null, + LdapDnsProvider.DNSPROVIDER_PERMISSION, + SecurityConstants.GET_CLASSLOADER_PERMISSION); + } + } + + /** + * Retrieve the singleton static instance of LdapDnsProviderService. + */ + protected static synchronized LdapDnsProviderService getInstance() { + if (service != null) return service; + synchronized(lock) { + if (service != null) return service; + service = new LdapDnsProviderService(); + } + return service; + } + + /** + * Retrieve result from the first provider that successfully resolves + * the endpoints. If no results are found when calling installed + * subclasses of {@code LdapDnsProvider} then this method will fall back + * to the {@code DefaultLdapDnsProvider}. + */ + protected LdapDnsProviderResult lookupEndpoints(String url, Hashtable env) + { + LdapDnsProviderResult result = null; + Iterator iterator = providers.iterator(); + + try { + while ((result == null || result.getEndpoints().size() == 0) + && iterator.hasNext()) + { + LdapDnsProvider p = iterator.next(); + result = p.lookupEndpoints(url, env); + } + } catch (ServiceConfigurationError serviceError) { + result = null; + } + + if (result == null) { + result = new DefaultLdapDnsProvider().lookupEndpoints(url, env); + } + + return result; + } +} --- /dev/null 2017-11-29 10:09:48.676000048 +0000 +++ new/src/java.naming/share/classes/javax/naming/ldap/LdapDnsProvider.java 2017-11-29 17:13:35.256982895 +0000 @@ -0,0 +1,62 @@ +package javax.naming.ldap; + +import java.util.Hashtable; +import javax.naming.ldap.LdapDnsProviderResult; + +/** + * DNS provider abstract class whose concrete implementations are used by + * {@code com.sun.jndi.ldap.LdapCtxFactory.getUsingURL()} to resolve ldap + * server urls. + * + * The ServiceLoader is used to create and register implementations of + * {@code LdapDnsProvider}. + * + * A DNS provider must subclass the {@link LdapDnsProvider} class. + * In addition, the provider class must be public and must have a + * public constructor that accepts no parameters. + */ +public abstract class LdapDnsProvider { + + /** + * The {@code RuntimePermission("ldapDnsProvider")} is + * necessary to subclass and instantiate the {@code LdapDnsProvider} class. + */ + public static final RuntimePermission DNSPROVIDER_PERMISSION = + new RuntimePermission("ldapDnsProvider"); + + /** + * Creates a new instance of {@code LdapDnsProvider}. + * + * @throws SecurityException if a security manager is present and its + * {@code checkPermission} method doesn't allow the + * {@code RuntimePermission("ldapDnsProvider")}. + */ + protected LdapDnsProvider() { + this(checkPermission()); + } + + private LdapDnsProvider(Void unused) { + // nothing to do. + } + + private static Void checkPermission() { + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(DNSPROVIDER_PERMISSION); + } + return null; + } + + /** + * The {@code lookupEndpoints(String url, Hashtable env)} method + * takes two parameters: + * + * @param url The {@code Context.PROVIDER_URL} + * @param env The LdapCtx environment properties. + * + * @return an {@link LdapDnsProviderResult}. + */ + public abstract LdapDnsProviderResult lookupEndpoints( + String url, Hashtable env); + +} --- /dev/null 2017-11-29 10:09:48.676000048 +0000 +++ new/src/java.naming/share/classes/javax/naming/ldap/LdapDnsProviderResult.java 2017-11-29 17:13:35.516984095 +0000 @@ -0,0 +1,45 @@ +package javax.naming.ldap; + +import java.util.List; + +/** + * This class is used by an {@link LdapDnsProvider} to return the result of a + * DNS lookup for a given ldap url. The result consists of a domain name and + * its associated ldap server endpoints. + */ +public class LdapDnsProviderResult { + + private String domainName; + private List endpoints; + + /** + * Construct an LdapDnsProviderResult consisting of a resolved domain name + * and the ldap server endpoints that serve the domain. + * + * @param domainName the resolved domain name + * @param endpoints the resolved ldap server endpoints + */ + public LdapDnsProviderResult(String domainName, List endpoints) { + this.domainName = domainName; + this.endpoints = endpoints; + } + + /** + * Get the domain name resolved from the ldap URL. + * + * @return the resolved domain name + */ + public String getDomainName() { + return domainName; + } + + /** + * Get the individual server endpoints resolved from the ldap URL. + * + * @return the resolved ldap server endpoints + */ + public List getEndpoints() { + return endpoints; + } + +} --- /dev/null 2017-11-29 10:09:48.676000048 +0000 +++ new/test/jdk/com/sun/jndi/ldap/LdapDnsProviderTest.java 2017-11-29 17:13:35.772985279 +0000 @@ -0,0 +1,192 @@ +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.Permission; +import java.util.Hashtable; +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; + +/** + * @test + * @compile dnsprovider/TestDnsProvider.java + * @run main/othervm LdapDnsProviderTest + * @run main/othervm LdapDnsProviderTest nosm + * @run main/othervm LdapDnsProviderTest smnodns + * @run main/othervm LdapDnsProviderTest smdns + * @modules java.naming/com.sun.jndi.ldap + * @bug 8160768 + * @summary ctx provider tests for ldap + */ +class DNSSecurityManager extends SecurityManager { + private boolean dnsProvider = false; + private String perm = + javax.naming.ldap.LdapDnsProvider.DNSPROVIDER_PERMISSION; + + public void setAllowDnsProvider(boolean allow) { + dnsProvider = allow; + } + + @Override + public void checkPermission(Permission p) { + if (p.getName().equals(perm) && !dnsProvider) { + throw new SecurityException(p.getName()); + } + } +} + +class ProviderTest implements Callable { + + private final String expected; + // private final LdapTestServer server; + private final Hashtable env = new Hashtable(11); + + public ProviderTest(String expected) throws IOException { + // this.server = new LdapTestServer(); + this.expected = expected; + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + } + + boolean shutItDown(InitialContext ctx) { + try { + if (ctx != null) ctx.close(); + return true; + } catch (NamingException ex) { + return false; + } + } + + public Boolean call() { + boolean passed; + InitialContext ctx = null; + // String providerUrl = "ldap://localhost:" + server.getLocalPort(); + String providerUrl = "ldap:///dc=example,dc=com"; + + try { + env.put(Context.PROVIDER_URL, providerUrl); + + try { + ctx = new InitialDirContext(env); + SearchControls scl = new SearchControls(); + scl.setSearchScope(SearchControls.SUBTREE_SCOPE); + ((InitialDirContext)ctx).search( + "ou=People,o=Test", "(objectClass=*)", scl); + throw new RuntimeException("Search should not complete"); + } catch (NamingException e) { + System.out.println(e); + e.printStackTrace(); + passed = e.toString().indexOf(expected) > -1; + } finally { + shutItDown(ctx); + } + return passed; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} + +public class LdapDnsProviderTest { + + private static final String testClasses = + System.getProperty("test.classes", "."); + private static final String testSrc = + System.getProperty("test.src", "."); + + + public static void copyFile(File srcFile, File dstFile) + throws IOException + { + FileInputStream src = new FileInputStream(srcFile); + FileOutputStream dst = new FileOutputStream(dstFile); + + byte[] buf = new byte[32768]; + while (true) { + int count = src.read(buf); + if (count < 0) { + break; + } + dst.write(buf, 0, count); + } + + dst.close(); + src.close(); + } + + public static void installServiceConfigurationFile() { + String filename = "javax.naming.ldap.LdapDnsProvider"; + + File dstDir = new File(testClasses, "META-INF/services"); + if (!dstDir.exists()) { + if (!dstDir.mkdirs()) { + throw new RuntimeException( + "could not create META-INF/services directory " + dstDir); + } + } + File dstFile = new File(dstDir, filename); + + File srcDir = new File(testSrc); + File srcFile = new File(srcDir, filename); + + try { + copyFile(srcFile, dstFile); + } catch (IOException e) { + throw new RuntimeException("could not install " + dstFile, e); + } + } + + public static void main(String[] args) throws Exception { + if (args.length > 0 && args[0].equals("nosm")) { + // no security manager, serviceloader + installServiceConfigurationFile(); + runTest("yupyupyup:389"); + } else if (args.length > 0 && args[0].equals("smnodns")) { + // security manager & serviceloader + installServiceConfigurationFile(); + // install security manager + System.setSecurityManager(new DNSSecurityManager()); + runTest("ldapDnsProvider"); + } else if (args.length > 0 && args[0].equals("smdns")) { + // security manager & serviceloader + DNSSecurityManager sm = new DNSSecurityManager(); + installServiceConfigurationFile(); + // install security manager + System.setSecurityManager(sm); + sm.setAllowDnsProvider(true); + runTest("yupyupyup:389"); + } else { + // no security manager, no serviceloader + + // DefaultLdapDnsProvider + File f = new File( + testClasses, "META-INF/services/javax.naming.ldap.LdapDnsProvider"); + if (f.exists()) { + f.delete(); + } + + // no SecurityManager + runTest("localhost:389"); + } + } + + private static boolean runTest(String expected) throws Exception { + FutureTask future = + new FutureTask(new ProviderTest(expected)); + new Thread(future).start(); + + while (!future.isDone()) { + if ((Boolean) future.get()) { + return true; + } + } + throw new AssertionError("FAILED: " + expected); + } + +} + --- /dev/null 2017-11-29 10:09:48.676000048 +0000 +++ new/test/jdk/com/sun/jndi/ldap/dnsprovider/TestDnsProvider.java 2017-11-29 17:13:36.036986499 +0000 @@ -0,0 +1,17 @@ +package dnsprovider; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import javax.naming.ldap.LdapDnsProvider; +import javax.naming.ldap.LdapDnsProviderResult; + +public class TestDnsProvider extends LdapDnsProvider { + @Override + public LdapDnsProviderResult lookupEndpoints(String url, Hashtable env) + { + List endpoints = new ArrayList<>(); + endpoints.add("ldap://yupyupyup:389"); + return new LdapDnsProviderResult("test.com", endpoints); + } +} --- /dev/null 2017-11-29 10:09:48.676000048 +0000 +++ new/test/jdk/com/sun/jndi/ldap/javax.naming.ldap.LdapDnsProvider 2017-11-29 17:13:36.296987701 +0000 @@ -0,0 +1 @@ +dnsprovider.TestDnsProvider \ No newline at end of file