• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2015 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc.internal;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 
21 import com.google.common.annotations.VisibleForTesting;
22 import com.google.common.base.MoreObjects;
23 import com.google.common.base.Objects;
24 import com.google.common.base.Preconditions;
25 import com.google.common.base.Stopwatch;
26 import com.google.common.base.Throwables;
27 import com.google.common.base.Verify;
28 import com.google.common.base.VerifyException;
29 import io.grpc.Attributes;
30 import io.grpc.EquivalentAddressGroup;
31 import io.grpc.NameResolver;
32 import io.grpc.ProxiedSocketAddress;
33 import io.grpc.ProxyDetector;
34 import io.grpc.Status;
35 import io.grpc.SynchronizationContext;
36 import io.grpc.internal.SharedResourceHolder.Resource;
37 import java.io.IOException;
38 import java.lang.reflect.Constructor;
39 import java.net.InetAddress;
40 import java.net.InetSocketAddress;
41 import java.net.URI;
42 import java.net.UnknownHostException;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Collections;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Random;
50 import java.util.Set;
51 import java.util.concurrent.Executor;
52 import java.util.concurrent.TimeUnit;
53 import java.util.concurrent.atomic.AtomicReference;
54 import java.util.logging.Level;
55 import java.util.logging.Logger;
56 import javax.annotation.Nullable;
57 
58 /**
59  * A DNS-based {@link NameResolver}.
60  *
61  * <p>Each {@code A} or {@code AAAA} record emits an {@link EquivalentAddressGroup} in the list
62  * passed to {@link NameResolver.Listener2#onResult(ResolutionResult)}.
63  *
64  * @see DnsNameResolverProvider
65  */
66 public class DnsNameResolver extends NameResolver {
67 
68   private static final Logger logger = Logger.getLogger(DnsNameResolver.class.getName());
69 
70   private static final String SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY = "clientLanguage";
71   private static final String SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY = "percentage";
72   private static final String SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY = "clientHostname";
73   private static final String SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY = "serviceConfig";
74 
75   // From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
76   static final String SERVICE_CONFIG_PREFIX = "grpc_config=";
77   private static final Set<String> SERVICE_CONFIG_CHOICE_KEYS =
78       Collections.unmodifiableSet(
79           new HashSet<>(
80               Arrays.asList(
81                   SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY,
82                   SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY,
83                   SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY,
84                   SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY)));
85 
86   // From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
87   private static final String SERVICE_CONFIG_NAME_PREFIX = "_grpc_config.";
88 
89   private static final String JNDI_PROPERTY =
90       System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi", "true");
91   private static final String JNDI_LOCALHOST_PROPERTY =
92       System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi_localhost", "false");
93   private static final String JNDI_TXT_PROPERTY =
94       System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_service_config", "false");
95 
96   /**
97    * Java networking system properties name for caching DNS result.
98    *
99    * <p>Default value is -1 (cache forever) if security manager is installed. If security manager is
100    * not installed, the ttl value is {@code null} which falls back to {@link
101    * #DEFAULT_NETWORK_CACHE_TTL_SECONDS gRPC default value}.
102    *
103    * <p>For android, gRPC doesn't attempt to cache; this property value will be ignored.
104    */
105   @VisibleForTesting
106   static final String NETWORKADDRESS_CACHE_TTL_PROPERTY = "networkaddress.cache.ttl";
107   /** Default DNS cache duration if network cache ttl value is not specified ({@code null}). */
108   @VisibleForTesting
109   static final long DEFAULT_NETWORK_CACHE_TTL_SECONDS = 30;
110 
111   @VisibleForTesting
112   static boolean enableJndi = Boolean.parseBoolean(JNDI_PROPERTY);
113   @VisibleForTesting
114   static boolean enableJndiLocalhost = Boolean.parseBoolean(JNDI_LOCALHOST_PROPERTY);
115   @VisibleForTesting
116   protected static boolean enableTxt = Boolean.parseBoolean(JNDI_TXT_PROPERTY);
117 
118   private static final ResourceResolverFactory resourceResolverFactory =
119       getResourceResolverFactory(DnsNameResolver.class.getClassLoader());
120 
121   @VisibleForTesting
122   final ProxyDetector proxyDetector;
123 
124   /** Access through {@link #getLocalHostname}. */
125   private static String localHostname;
126 
127   private final Random random = new Random();
128 
129   protected volatile AddressResolver addressResolver = JdkAddressResolver.INSTANCE;
130   private final AtomicReference<ResourceResolver> resourceResolver = new AtomicReference<>();
131 
132   private final String authority;
133   private final String host;
134   private final int port;
135 
136   /** Executor that will be used if an Executor is not provide via {@link NameResolver.Args}. */
137   private final Resource<Executor> executorResource;
138   private final long cacheTtlNanos;
139   private final SynchronizationContext syncContext;
140 
141   // Following fields must be accessed from syncContext
142   private final Stopwatch stopwatch;
143   protected boolean resolved;
144   private boolean shutdown;
145   private Executor executor;
146 
147   /** True if using an executor resource that should be released after use. */
148   private final boolean usingExecutorResource;
149   private final ServiceConfigParser serviceConfigParser;
150 
151   private boolean resolving;
152 
153   // The field must be accessed from syncContext, although the methods on an Listener2 can be called
154   // from any thread.
155   private NameResolver.Listener2 listener;
156 
DnsNameResolver( @ullable String nsAuthority, String name, Args args, Resource<Executor> executorResource, Stopwatch stopwatch, boolean isAndroid)157   protected DnsNameResolver(
158       @Nullable String nsAuthority,
159       String name,
160       Args args,
161       Resource<Executor> executorResource,
162       Stopwatch stopwatch,
163       boolean isAndroid) {
164     checkNotNull(args, "args");
165     // TODO: if a DNS server is provided as nsAuthority, use it.
166     // https://www.captechconsulting.com/blogs/accessing-the-dusty-corners-of-dns-with-java
167     this.executorResource = executorResource;
168     // Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an
169     // opaque URI, thus the authority and host of the resulted URI would be null.
170     URI nameUri = URI.create("//" + checkNotNull(name, "name"));
171     Preconditions.checkArgument(nameUri.getHost() != null, "Invalid DNS name: %s", name);
172     authority = Preconditions.checkNotNull(nameUri.getAuthority(),
173         "nameUri (%s) doesn't have an authority", nameUri);
174     host = nameUri.getHost();
175     if (nameUri.getPort() == -1) {
176       port = args.getDefaultPort();
177     } else {
178       port = nameUri.getPort();
179     }
180     this.proxyDetector = checkNotNull(args.getProxyDetector(), "proxyDetector");
181     this.cacheTtlNanos = getNetworkAddressCacheTtlNanos(isAndroid);
182     this.stopwatch = checkNotNull(stopwatch, "stopwatch");
183     this.syncContext = checkNotNull(args.getSynchronizationContext(), "syncContext");
184     this.executor = args.getOffloadExecutor();
185     this.usingExecutorResource = executor == null;
186     this.serviceConfigParser = checkNotNull(args.getServiceConfigParser(), "serviceConfigParser");
187   }
188 
189   @Override
getServiceAuthority()190   public String getServiceAuthority() {
191     return authority;
192   }
193 
194   @VisibleForTesting
getHost()195   protected String getHost() {
196     return host;
197   }
198 
199   @Override
start(Listener2 listener)200   public void start(Listener2 listener) {
201     Preconditions.checkState(this.listener == null, "already started");
202     if (usingExecutorResource) {
203       executor = SharedResourceHolder.get(executorResource);
204     }
205     this.listener = checkNotNull(listener, "listener");
206     resolve();
207   }
208 
209   @Override
refresh()210   public void refresh() {
211     Preconditions.checkState(listener != null, "not started");
212     resolve();
213   }
214 
resolveAddresses()215   private List<EquivalentAddressGroup> resolveAddresses() {
216     List<? extends InetAddress> addresses;
217     Exception addressesException = null;
218     try {
219       addresses = addressResolver.resolveAddress(host);
220     } catch (Exception e) {
221       addressesException = e;
222       Throwables.throwIfUnchecked(e);
223       throw new RuntimeException(e);
224     } finally {
225       if (addressesException != null) {
226         logger.log(Level.FINE, "Address resolution failure", addressesException);
227       }
228     }
229     // Each address forms an EAG
230     List<EquivalentAddressGroup> servers = new ArrayList<>(addresses.size());
231     for (InetAddress inetAddr : addresses) {
232       servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port)));
233     }
234     return Collections.unmodifiableList(servers);
235   }
236 
237   @Nullable
resolveServiceConfig()238   private ConfigOrError resolveServiceConfig() {
239     List<String> txtRecords = Collections.emptyList();
240     ResourceResolver resourceResolver = getResourceResolver();
241     if (resourceResolver != null) {
242       try {
243         txtRecords = resourceResolver.resolveTxt(SERVICE_CONFIG_NAME_PREFIX + host);
244       } catch (Exception e) {
245         logger.log(Level.FINE, "ServiceConfig resolution failure", e);
246       }
247     }
248     if (!txtRecords.isEmpty()) {
249       ConfigOrError rawServiceConfig = parseServiceConfig(txtRecords, random, getLocalHostname());
250       if (rawServiceConfig != null) {
251         if (rawServiceConfig.getError() != null) {
252           return ConfigOrError.fromError(rawServiceConfig.getError());
253         }
254 
255         @SuppressWarnings("unchecked")
256         Map<String, ?> verifiedRawServiceConfig = (Map<String, ?>) rawServiceConfig.getConfig();
257         return serviceConfigParser.parseServiceConfig(verifiedRawServiceConfig);
258       }
259     } else {
260       logger.log(Level.FINE, "No TXT records found for {0}", new Object[]{host});
261     }
262     return null;
263   }
264 
265   @Nullable
detectProxy()266   private EquivalentAddressGroup detectProxy() throws IOException {
267     InetSocketAddress destination =
268         InetSocketAddress.createUnresolved(host, port);
269     ProxiedSocketAddress proxiedAddr = proxyDetector.proxyFor(destination);
270     if (proxiedAddr != null) {
271       return new EquivalentAddressGroup(proxiedAddr);
272     }
273     return null;
274   }
275 
276   /**
277    * Main logic of name resolution.
278    */
doResolve(boolean forceTxt)279   protected InternalResolutionResult doResolve(boolean forceTxt) {
280     InternalResolutionResult result = new InternalResolutionResult();
281     try {
282       result.addresses = resolveAddresses();
283     } catch (Exception e) {
284       if (!forceTxt) {
285         result.error =
286             Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e);
287         return result;
288       }
289     }
290     if (enableTxt) {
291       result.config = resolveServiceConfig();
292     }
293     return result;
294   }
295 
296   private final class Resolve implements Runnable {
297     private final Listener2 savedListener;
298 
Resolve(Listener2 savedListener)299     Resolve(Listener2 savedListener) {
300       this.savedListener = checkNotNull(savedListener, "savedListener");
301     }
302 
303     @Override
run()304     public void run() {
305       if (logger.isLoggable(Level.FINER)) {
306         logger.finer("Attempting DNS resolution of " + host);
307       }
308       InternalResolutionResult result = null;
309       try {
310         EquivalentAddressGroup proxiedAddr = detectProxy();
311         ResolutionResult.Builder resolutionResultBuilder = ResolutionResult.newBuilder();
312         if (proxiedAddr != null) {
313           if (logger.isLoggable(Level.FINER)) {
314             logger.finer("Using proxy address " + proxiedAddr);
315           }
316           resolutionResultBuilder.setAddresses(Collections.singletonList(proxiedAddr));
317         } else {
318           result = doResolve(false);
319           if (result.error != null) {
320             savedListener.onError(result.error);
321             return;
322           }
323           if (result.addresses != null) {
324             resolutionResultBuilder.setAddresses(result.addresses);
325           }
326           if (result.config != null) {
327             resolutionResultBuilder.setServiceConfig(result.config);
328           }
329           if (result.attributes != null) {
330             resolutionResultBuilder.setAttributes(result.attributes);
331           }
332         }
333         savedListener.onResult(resolutionResultBuilder.build());
334       } catch (IOException e) {
335         savedListener.onError(
336             Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e));
337       } finally {
338         final boolean succeed = result != null && result.error == null;
339         syncContext.execute(new Runnable() {
340           @Override
341           public void run() {
342             if (succeed) {
343               resolved = true;
344               if (cacheTtlNanos > 0) {
345                 stopwatch.reset().start();
346               }
347             }
348             resolving = false;
349           }
350         });
351       }
352     }
353   }
354 
355   @Nullable
parseServiceConfig( List<String> rawTxtRecords, Random random, String localHostname)356   static ConfigOrError parseServiceConfig(
357       List<String> rawTxtRecords, Random random, String localHostname) {
358     List<Map<String, ?>> possibleServiceConfigChoices;
359     try {
360       possibleServiceConfigChoices = parseTxtResults(rawTxtRecords);
361     } catch (IOException | RuntimeException e) {
362       return ConfigOrError.fromError(
363           Status.UNKNOWN.withDescription("failed to parse TXT records").withCause(e));
364     }
365     Map<String, ?> possibleServiceConfig = null;
366     for (Map<String, ?> possibleServiceConfigChoice : possibleServiceConfigChoices) {
367       try {
368         possibleServiceConfig =
369             maybeChooseServiceConfig(possibleServiceConfigChoice, random, localHostname);
370       } catch (RuntimeException e) {
371         return ConfigOrError.fromError(
372             Status.UNKNOWN.withDescription("failed to pick service config choice").withCause(e));
373       }
374       if (possibleServiceConfig != null) {
375         break;
376       }
377     }
378     if (possibleServiceConfig == null) {
379       return null;
380     }
381     return ConfigOrError.fromConfig(possibleServiceConfig);
382   }
383 
resolve()384   private void resolve() {
385     if (resolving || shutdown || !cacheRefreshRequired()) {
386       return;
387     }
388     resolving = true;
389     executor.execute(new Resolve(listener));
390   }
391 
cacheRefreshRequired()392   private boolean cacheRefreshRequired() {
393     return !resolved
394         || cacheTtlNanos == 0
395         || (cacheTtlNanos > 0 && stopwatch.elapsed(TimeUnit.NANOSECONDS) > cacheTtlNanos);
396   }
397 
398   @Override
shutdown()399   public void shutdown() {
400     if (shutdown) {
401       return;
402     }
403     shutdown = true;
404     if (executor != null && usingExecutorResource) {
405       executor = SharedResourceHolder.release(executorResource, executor);
406     }
407   }
408 
getPort()409   final int getPort() {
410     return port;
411   }
412 
413   /**
414    * Parse TXT service config records as JSON.
415    *
416    * @throws IOException if one of the txt records contains improperly formatted JSON.
417    */
418   @VisibleForTesting
parseTxtResults(List<String> txtRecords)419   static List<Map<String, ?>> parseTxtResults(List<String> txtRecords) throws IOException {
420     List<Map<String, ?>> possibleServiceConfigChoices = new ArrayList<>();
421     for (String txtRecord : txtRecords) {
422       if (!txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
423         logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
424         continue;
425       }
426       Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
427       if (!(rawChoices instanceof List)) {
428         throw new ClassCastException("wrong type " + rawChoices);
429       }
430       List<?> listChoices = (List<?>) rawChoices;
431       possibleServiceConfigChoices.addAll(JsonUtil.checkObjectList(listChoices));
432     }
433     return possibleServiceConfigChoices;
434   }
435 
436   @Nullable
getPercentageFromChoice(Map<String, ?> serviceConfigChoice)437   private static final Double getPercentageFromChoice(Map<String, ?> serviceConfigChoice) {
438     return JsonUtil.getNumberAsDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY);
439   }
440 
441   @Nullable
getClientLanguagesFromChoice( Map<String, ?> serviceConfigChoice)442   private static final List<String> getClientLanguagesFromChoice(
443       Map<String, ?> serviceConfigChoice) {
444     return JsonUtil.getListOfStrings(
445         serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY);
446   }
447 
448   @Nullable
getHostnamesFromChoice(Map<String, ?> serviceConfigChoice)449   private static final List<String> getHostnamesFromChoice(Map<String, ?> serviceConfigChoice) {
450     return JsonUtil.getListOfStrings(
451         serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY);
452   }
453 
454   /**
455    * Returns value of network address cache ttl property if not Android environment. For android,
456    * DnsNameResolver does not cache the dns lookup result.
457    */
getNetworkAddressCacheTtlNanos(boolean isAndroid)458   private static long getNetworkAddressCacheTtlNanos(boolean isAndroid) {
459     if (isAndroid) {
460       // on Android, ignore dns cache.
461       return 0;
462     }
463 
464     String cacheTtlPropertyValue = System.getProperty(NETWORKADDRESS_CACHE_TTL_PROPERTY);
465     long cacheTtl = DEFAULT_NETWORK_CACHE_TTL_SECONDS;
466     if (cacheTtlPropertyValue != null) {
467       try {
468         cacheTtl = Long.parseLong(cacheTtlPropertyValue);
469       } catch (NumberFormatException e) {
470         logger.log(
471             Level.WARNING,
472             "Property({0}) valid is not valid number format({1}), fall back to default({2})",
473             new Object[] {NETWORKADDRESS_CACHE_TTL_PROPERTY, cacheTtlPropertyValue, cacheTtl});
474       }
475     }
476     return cacheTtl > 0 ? TimeUnit.SECONDS.toNanos(cacheTtl) : cacheTtl;
477   }
478 
479   /**
480    * Determines if a given Service Config choice applies, and if so, returns it.
481    *
482    * @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
483    *   Service Config in DNS</a>
484    * @param choice The service config choice.
485    * @return The service config object or {@code null} if this choice does not apply.
486    */
487   @Nullable
488   @VisibleForTesting
maybeChooseServiceConfig( Map<String, ?> choice, Random random, String hostname)489   static Map<String, ?> maybeChooseServiceConfig(
490       Map<String, ?> choice, Random random, String hostname) {
491     for (Map.Entry<String, ?> entry : choice.entrySet()) {
492       Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
493     }
494 
495     List<String> clientLanguages = getClientLanguagesFromChoice(choice);
496     if (clientLanguages != null && !clientLanguages.isEmpty()) {
497       boolean javaPresent = false;
498       for (String lang : clientLanguages) {
499         if ("java".equalsIgnoreCase(lang)) {
500           javaPresent = true;
501           break;
502         }
503       }
504       if (!javaPresent) {
505         return null;
506       }
507     }
508     Double percentage = getPercentageFromChoice(choice);
509     if (percentage != null) {
510       int pct = percentage.intValue();
511       Verify.verify(pct >= 0 && pct <= 100, "Bad percentage: %s", percentage);
512       if (random.nextInt(100) >= pct) {
513         return null;
514       }
515     }
516     List<String> clientHostnames = getHostnamesFromChoice(choice);
517     if (clientHostnames != null && !clientHostnames.isEmpty()) {
518       boolean hostnamePresent = false;
519       for (String clientHostname : clientHostnames) {
520         if (clientHostname.equals(hostname)) {
521           hostnamePresent = true;
522           break;
523         }
524       }
525       if (!hostnamePresent) {
526         return null;
527       }
528     }
529     Map<String, ?> sc =
530         JsonUtil.getObject(choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY);
531     if (sc == null) {
532       throw new VerifyException(String.format(
533           "key '%s' missing in '%s'", choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY));
534     }
535     return sc;
536   }
537 
538   /**
539    * Used as a DNS-based name resolver's internal representation of resolution result.
540    */
541   protected static final class InternalResolutionResult {
542     private Status error;
543     private List<EquivalentAddressGroup> addresses;
544     private ConfigOrError config;
545     public Attributes attributes;
546 
InternalResolutionResult()547     private InternalResolutionResult() {}
548   }
549 
550   /**
551    * Describes a parsed SRV record.
552    */
553   @VisibleForTesting
554   public static final class SrvRecord {
555     public final String host;
556     public final int port;
557 
SrvRecord(String host, int port)558     public SrvRecord(String host, int port) {
559       this.host = host;
560       this.port = port;
561     }
562 
563     @Override
hashCode()564     public int hashCode() {
565       return Objects.hashCode(host, port);
566     }
567 
568     @Override
equals(Object obj)569     public boolean equals(Object obj) {
570       if (this == obj) {
571         return true;
572       }
573       if (obj == null || getClass() != obj.getClass()) {
574         return false;
575       }
576       SrvRecord that = (SrvRecord) obj;
577       return port == that.port && host.equals(that.host);
578     }
579 
580     @Override
toString()581     public String toString() {
582       return
583           MoreObjects.toStringHelper(this)
584               .add("host", host)
585               .add("port", port)
586               .toString();
587     }
588   }
589 
590   @VisibleForTesting
setAddressResolver(AddressResolver addressResolver)591   protected void setAddressResolver(AddressResolver addressResolver) {
592     this.addressResolver = addressResolver;
593   }
594 
595   @VisibleForTesting
setResourceResolver(ResourceResolver resourceResolver)596   protected void setResourceResolver(ResourceResolver resourceResolver) {
597     this.resourceResolver.set(resourceResolver);
598   }
599 
600   /**
601    * {@link ResourceResolverFactory} is a factory for making resource resolvers.  It supports
602    * optionally checking if the factory is available.
603    */
604   interface ResourceResolverFactory {
605 
606     /**
607      * Creates a new resource resolver.  The return value is {@code null} iff
608      * {@link #unavailabilityCause()} is not null;
609      */
newResourceResolver()610     @Nullable ResourceResolver newResourceResolver();
611 
612     /**
613      * Returns the reason why the resource resolver cannot be created.  The return value is
614      * {@code null} if {@link #newResourceResolver()} is suitable for use.
615      */
unavailabilityCause()616     @Nullable Throwable unavailabilityCause();
617   }
618 
619   /**
620    * AddressResolver resolves a hostname into a list of addresses.
621    */
622   @VisibleForTesting
623   public interface AddressResolver {
resolveAddress(String host)624     List<InetAddress> resolveAddress(String host) throws Exception;
625   }
626 
627   private enum JdkAddressResolver implements AddressResolver {
628     INSTANCE;
629 
630     @Override
resolveAddress(String host)631     public List<InetAddress> resolveAddress(String host) throws UnknownHostException {
632       return Collections.unmodifiableList(Arrays.asList(InetAddress.getAllByName(host)));
633     }
634   }
635 
636   /**
637    * {@link ResourceResolver} is a Dns ResourceRecord resolver.
638    */
639   @VisibleForTesting
640   public interface ResourceResolver {
resolveTxt(String host)641     List<String> resolveTxt(String host) throws Exception;
642 
resolveSrv(String host)643     List<SrvRecord> resolveSrv(String host) throws Exception;
644   }
645 
646   @Nullable
getResourceResolver()647   protected ResourceResolver getResourceResolver() {
648     if (!shouldUseJndi(enableJndi, enableJndiLocalhost, host)) {
649       return null;
650     }
651     ResourceResolver rr;
652     if ((rr = resourceResolver.get()) == null) {
653       if (resourceResolverFactory != null) {
654         assert resourceResolverFactory.unavailabilityCause() == null;
655         rr = resourceResolverFactory.newResourceResolver();
656       }
657     }
658     return rr;
659   }
660 
661   @Nullable
662   @VisibleForTesting
getResourceResolverFactory(ClassLoader loader)663   static ResourceResolverFactory getResourceResolverFactory(ClassLoader loader) {
664     Class<? extends ResourceResolverFactory> jndiClazz;
665     try {
666       jndiClazz =
667           Class.forName("io.grpc.internal.JndiResourceResolverFactory", true, loader)
668               .asSubclass(ResourceResolverFactory.class);
669     } catch (ClassNotFoundException e) {
670       logger.log(Level.FINE, "Unable to find JndiResourceResolverFactory, skipping.", e);
671       return null;
672     } catch (ClassCastException e) {
673       // This can happen if JndiResourceResolverFactory was removed by something like Proguard
674       // combined with a broken ClassLoader that prefers classes from the child over the parent
675       // while also not properly filtering dependencies in the parent that should be hidden. If the
676       // class loader prefers loading from the parent then ResourceresolverFactory would have also
677       // been loaded from the parent. If the class loader filtered deps, then
678       // JndiResourceResolverFactory wouldn't have been found.
679       logger.log(Level.FINE, "Unable to cast JndiResourceResolverFactory, skipping.", e);
680       return null;
681     }
682     Constructor<? extends ResourceResolverFactory> jndiCtor;
683     try {
684       jndiCtor = jndiClazz.getConstructor();
685     } catch (Exception e) {
686       logger.log(Level.FINE, "Can't find JndiResourceResolverFactory ctor, skipping.", e);
687       return null;
688     }
689     ResourceResolverFactory rrf;
690     try {
691       rrf = jndiCtor.newInstance();
692     } catch (Exception e) {
693       logger.log(Level.FINE, "Can't construct JndiResourceResolverFactory, skipping.", e);
694       return null;
695     }
696     if (rrf.unavailabilityCause() != null) {
697       logger.log(
698           Level.FINE,
699           "JndiResourceResolverFactory not available, skipping.",
700           rrf.unavailabilityCause());
701       return null;
702     }
703     return rrf;
704   }
705 
getLocalHostname()706   private static String getLocalHostname() {
707     if (localHostname == null) {
708       try {
709         localHostname = InetAddress.getLocalHost().getHostName();
710       } catch (UnknownHostException e) {
711         throw new RuntimeException(e);
712       }
713     }
714     return localHostname;
715   }
716 
717   @VisibleForTesting
shouldUseJndi( boolean jndiEnabled, boolean jndiLocalhostEnabled, String target)718   protected static boolean shouldUseJndi(
719       boolean jndiEnabled, boolean jndiLocalhostEnabled, String target) {
720     if (!jndiEnabled) {
721       return false;
722     }
723     if ("localhost".equalsIgnoreCase(target)) {
724       return jndiLocalhostEnabled;
725     }
726     // Check if this name looks like IPv6
727     if (target.contains(":")) {
728       return false;
729     }
730     // Check if this might be IPv4.  Such addresses have no alphabetic characters.  This also
731     // checks the target is empty.
732     boolean alldigits = true;
733     for (int i = 0; i < target.length(); i++) {
734       char c = target.charAt(i);
735       if (c != '.') {
736         alldigits &= (c >= '0' && c <= '9');
737       }
738     }
739     return !alldigits;
740   }
741 }
742