package com.lambdaworks.redis.resource; import java.io.Closeable; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Properties; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.InitialDirContext; import com.google.common.net.InetAddresses; import com.lambdaworks.redis.LettuceStrings; import com.lambdaworks.redis.internal.LettuceAssert; /** * DNS Resolver based on Java's {@link com.sun.jndi.dns.DnsContextFactory}. This resolver resolves hostnames to IPv4 and IPv6 * addresses using {@code A}, {@code AAAA} and {@code CNAME} records. Java IP stack preferences are read from system properties * and taken into account when resolving names. * <p> * The default configuration uses system-configured DNS server addresses to perform lookups but server adresses can be specified * using {@link #DirContextDnsResolver(Iterable)}. Custom DNS servers can be specified by using * {@link #DirContextDnsResolver(String)} or {@link #DirContextDnsResolver(Iterable)}. * </p> * * @author Mark Paluch * @since 4.2 */ public class DirContextDnsResolver implements DnsResolver, Closeable { static final String PREFER_IPV4_KEY = "java.net.preferIPv4Stack"; static final String PREFER_IPV6_KEY = "java.net.preferIPv6Stack"; private static final String CTX_FACTORY_NAME = "com.sun.jndi.dns.DnsContextFactory"; private static final String INITIAL_TIMEOUT = "com.sun.jndi.dns.timeout.initial"; private static final String LOOKUP_RETRIES = "com.sun.jndi.dns.timeout.retries"; private static final String DEFAULT_INITIAL_TIMEOUT = "1000"; private static final String DEFAULT_RETRIES = "4"; private final boolean preferIpv4; private final boolean preferIpv6; private final Properties properties; private final InitialDirContext context; /** * Creates a new {@link DirContextDnsResolver} using system-configured DNS servers. */ public DirContextDnsResolver() { this(new Properties(), new StackPreference()); } /** * Creates a new {@link DirContextDnsResolver} using a collection of DNS servers. * * @param dnsServer must not be {@literal null} and not empty. */ public DirContextDnsResolver(String dnsServer) { this(Collections.singleton(dnsServer)); } /** * Creates a new {@link DirContextDnsResolver} using a collection of DNS servers. * * @param dnsServers must not be {@literal null} and not empty. */ public DirContextDnsResolver(Iterable<String> dnsServers) { this(getProperties(dnsServers), new StackPreference()); } /** * Creates a new {@link DirContextDnsResolver} for the given stack preference and {@code properties}. * * @param preferIpv4 flag to prefer IPv4 over IPv6 address resolution. * @param preferIpv6 flag to prefer IPv6 over IPv4 address resolution. * @param properties custom properties for creating the context, must not be {@literal null}. */ public DirContextDnsResolver(boolean preferIpv4, boolean preferIpv6, Properties properties) { this.preferIpv4 = preferIpv4; this.preferIpv6 = preferIpv6; this.properties = properties; this.context = createContext(properties); } private DirContextDnsResolver(Properties properties, StackPreference stackPreference) { this.properties = new Properties(properties); this.preferIpv4 = stackPreference.preferIpv4; this.preferIpv6 = stackPreference.preferIpv6; this.context = createContext(properties); } private InitialDirContext createContext(Properties properties) { LettuceAssert.notNull(properties, "Properties must not be null"); Properties hashtable = (Properties) properties.clone(); hashtable.put(InitialContext.INITIAL_CONTEXT_FACTORY, CTX_FACTORY_NAME); if (!hashtable.containsKey(INITIAL_TIMEOUT)) { hashtable.put(INITIAL_TIMEOUT, DEFAULT_INITIAL_TIMEOUT); } if (!hashtable.containsKey(LOOKUP_RETRIES)) { hashtable.put(LOOKUP_RETRIES, DEFAULT_RETRIES); } try { return new InitialDirContext(hashtable); } catch (NamingException e) { throw new IllegalStateException(e); } } @Override public void close() throws IOException { try { context.close(); } catch (NamingException e) { throw new IOException(e); } } /** * Perform hostname to address resolution. * * @param host the hostname, must not be empty or {@literal null}. * @return array of one or more {@link InetAddress adresses} * @throws UnknownHostException */ @Override public InetAddress[] resolve(String host) throws UnknownHostException { if (InetAddresses.isInetAddress(host)) { return new InetAddress[] { InetAddresses.forString(host) }; } List<InetAddress> inetAddresses = new ArrayList<>(); try { resolve(host, inetAddresses); } catch (NamingException e) { throw new UnknownHostException(String.format("Cannot resolve %s to a hostname because of %s", host, e)); } if (inetAddresses.isEmpty()) { throw new UnknownHostException(String.format("Cannot resolve %s to a hostname", host)); } return inetAddresses.toArray(new InetAddress[inetAddresses.size()]); } /** * Resolve a hostname * * @param hostname * @param inetAddresses * @throws NamingException * @throws UnknownHostException */ private void resolve(String hostname, List<InetAddress> inetAddresses) throws NamingException, UnknownHostException { if (preferIpv6 || (!preferIpv4 && !preferIpv6)) { inetAddresses.addAll(resolve(hostname, "AAAA")); inetAddresses.addAll(resolve(hostname, "A")); } else { inetAddresses.addAll(resolve(hostname, "A")); inetAddresses.addAll(resolve(hostname, "AAAA")); } if (inetAddresses.isEmpty()) { inetAddresses.addAll(resolveCname(hostname)); } } /** * Resolves {@code CNAME} records to {@link InetAddress adresses}. * * @param hostname * @return * @throws NamingException */ @SuppressWarnings("rawtypes") private List<InetAddress> resolveCname(String hostname) throws NamingException { List<InetAddress> inetAddresses = new ArrayList<>(); Attributes attrs = context.getAttributes(hostname, new String[] { "CNAME" }); Attribute attr = attrs.get("CNAME"); if (attr != null && attr.size() > 0) { NamingEnumeration e = attr.getAll(); while (e.hasMore()) { String h = (String) e.next(); if (h.endsWith(".")) { h = h.substring(0, h.lastIndexOf('.')); } try { InetAddress[] resolved = resolve(h); for (InetAddress inetAddress : resolved) { inetAddresses.add(InetAddress.getByAddress(hostname, inetAddress.getAddress())); } } catch (UnknownHostException e1) { // ignore } } } return inetAddresses; } /** * Resolve an attribute for a hostname. * * @param hostname * @param attrName * @return * @throws NamingException * @throws UnknownHostException */ @SuppressWarnings("rawtypes") private List<InetAddress> resolve(String hostname, String attrName) throws NamingException, UnknownHostException { Attributes attrs = context.getAttributes(hostname, new String[] { attrName }); List<InetAddress> inetAddresses = new ArrayList<>(); Attribute attr = attrs.get(attrName); if (attr != null && attr.size() > 0) { NamingEnumeration e = attr.getAll(); while (e.hasMore()) { InetAddress inetAddress = InetAddress.getByName("" + e.next()); inetAddresses.add(InetAddress.getByAddress(hostname, inetAddress.getAddress())); } } return inetAddresses; } private static Properties getProperties(Iterable<String> dnsServers) { Properties properties = new Properties(); StringBuffer providerUrl = new StringBuffer(); for (String dnsServer : dnsServers) { LettuceAssert.isTrue(LettuceStrings.isNotEmpty(dnsServer), "DNS Server must not be empty"); if (providerUrl.length() != 0) { providerUrl.append(' '); } providerUrl.append(String.format("dns://%s", dnsServer)); } if (providerUrl.length() == 0) { throw new IllegalArgumentException("DNS Servers must not be empty"); } properties.put(Context.PROVIDER_URL, providerUrl.toString()); return properties; } /** * Stack preference utility. */ private static final class StackPreference { final boolean preferIpv4; final boolean preferIpv6; public StackPreference() { boolean preferIpv4 = false; boolean preferIpv6 = false; if (System.getProperty(PREFER_IPV4_KEY) == null && System.getProperty(PREFER_IPV6_KEY) == null) { preferIpv4 = false; preferIpv6 = false; } if (System.getProperty(PREFER_IPV4_KEY) == null && System.getProperty(PREFER_IPV6_KEY) != null) { preferIpv6 = Boolean.getBoolean(PREFER_IPV6_KEY); if (!preferIpv6) { preferIpv4 = true; } } if (System.getProperty(PREFER_IPV4_KEY) != null && System.getProperty(PREFER_IPV6_KEY) == null) { preferIpv4 = Boolean.getBoolean(PREFER_IPV4_KEY); if (!preferIpv4) { preferIpv6 = true; } } if (System.getProperty(PREFER_IPV4_KEY) != null && System.getProperty(PREFER_IPV6_KEY) != null) { preferIpv4 = Boolean.getBoolean(PREFER_IPV4_KEY); preferIpv6 = Boolean.getBoolean(PREFER_IPV6_KEY); } this.preferIpv4 = preferIpv4; this.preferIpv6 = preferIpv6; } } }