• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 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 android.annotation.SuppressLint;
20 import com.google.common.annotations.VisibleForTesting;
21 import com.google.common.base.Verify;
22 import io.grpc.internal.DnsNameResolver.ResourceResolver;
23 import io.grpc.internal.DnsNameResolver.SrvRecord;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.Hashtable;
28 import java.util.List;
29 import java.util.logging.Level;
30 import java.util.logging.Logger;
31 import java.util.regex.Pattern;
32 import javax.annotation.Nullable;
33 import javax.naming.NamingEnumeration;
34 import javax.naming.NamingException;
35 import javax.naming.directory.Attribute;
36 import javax.naming.directory.DirContext;
37 import javax.naming.directory.InitialDirContext;
38 import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
39 
40 /**
41  * {@link JndiResourceResolverFactory} resolves additional records for the DnsNameResolver.
42  */
43 final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResolverFactory {
44 
45   @Nullable
46   @SuppressWarnings("StaticAssignmentOfThrowable")
47   private static final Throwable JNDI_UNAVAILABILITY_CAUSE = initJndi();
48 
49   // @UsedReflectively
JndiResourceResolverFactory()50   public JndiResourceResolverFactory() {}
51 
52   /**
53    * Returns whether the JNDI DNS resolver is available.  This is accomplished by looking up a
54    * particular class.  It is believed to be the default (only?) DNS resolver that will actually be
55    * used.  It is provided by the OpenJDK, but unlikely Android.  Actual resolution will be done by
56    * using a service provider when a hostname query is present, so the {@code DnsContextFactory}
57    * may not actually be used to perform the query.  This is believed to be "okay."
58    */
59   @Nullable
initJndi()60   private static Throwable initJndi() {
61     try {
62       Class.forName("javax.naming.directory.InitialDirContext");
63       Class.forName("com.sun.jndi.dns.DnsContextFactory");
64     } catch (ClassNotFoundException e) {
65       return e;
66     } catch (RuntimeException e) {
67       return e;
68     } catch (Error e) {
69       return e;
70     }
71     return null;
72   }
73 
74   @Nullable
75   @Override
newResourceResolver()76   public ResourceResolver newResourceResolver() {
77     if (unavailabilityCause() != null) {
78       return null;
79     }
80     return new JndiResourceResolver(new JndiRecordFetcher());
81   }
82 
83   @Nullable
84   @Override
unavailabilityCause()85   public Throwable unavailabilityCause() {
86     return JNDI_UNAVAILABILITY_CAUSE;
87   }
88 
89   @VisibleForTesting
90   interface RecordFetcher {
getAllRecords(String recordType, String name)91     List<String> getAllRecords(String recordType, String name) throws NamingException;
92   }
93 
94   @VisibleForTesting
95   static final class JndiResourceResolver implements DnsNameResolver.ResourceResolver {
96     private static final Logger logger =
97         Logger.getLogger(JndiResourceResolver.class.getName());
98 
99     private static final Pattern whitespace = Pattern.compile("\\s+");
100 
101     private final RecordFetcher recordFetcher;
102 
JndiResourceResolver(RecordFetcher recordFetcher)103     public JndiResourceResolver(RecordFetcher recordFetcher) {
104       this.recordFetcher = recordFetcher;
105     }
106 
107     @Override
resolveTxt(String serviceConfigHostname)108     public List<String> resolveTxt(String serviceConfigHostname) throws NamingException {
109       if (logger.isLoggable(Level.FINER)) {
110         logger.log(
111             Level.FINER, "About to query TXT records for {0}", new Object[]{serviceConfigHostname});
112       }
113       List<String> serviceConfigRawTxtRecords =
114           recordFetcher.getAllRecords("TXT", "dns:///" + serviceConfigHostname);
115       if (logger.isLoggable(Level.FINER)) {
116         logger.log(
117             Level.FINER, "Found {0} TXT records", new Object[]{serviceConfigRawTxtRecords.size()});
118       }
119       List<String> serviceConfigTxtRecords =
120           new ArrayList<>(serviceConfigRawTxtRecords.size());
121       for (String serviceConfigRawTxtRecord : serviceConfigRawTxtRecords) {
122         serviceConfigTxtRecords.add(unquote(serviceConfigRawTxtRecord));
123       }
124       return Collections.unmodifiableList(serviceConfigTxtRecords);
125     }
126 
127     @Override
resolveSrv(String host)128     public List<SrvRecord> resolveSrv(String host) throws Exception {
129       if (logger.isLoggable(Level.FINER)) {
130         logger.log(
131             Level.FINER, "About to query SRV records for {0}", new Object[]{host});
132       }
133       List<String> rawSrvRecords =
134           recordFetcher.getAllRecords("SRV", "dns:///" + host);
135       if (logger.isLoggable(Level.FINER)) {
136         logger.log(
137             Level.FINER, "Found {0} SRV records", new Object[]{rawSrvRecords.size()});
138       }
139       List<SrvRecord> srvRecords = new ArrayList<>(rawSrvRecords.size());
140       Exception first = null;
141       Level level = Level.WARNING;
142       for (String rawSrv : rawSrvRecords) {
143         try {
144           String[] parts = whitespace.split(rawSrv, 5);
145           Verify.verify(parts.length == 4, "Bad SRV Record: %s", rawSrv);
146           // SRV requires the host name to be absolute
147           if (!parts[3].endsWith(".")) {
148             throw new RuntimeException("Returned SRV host does not end in period: " + parts[3]);
149           }
150           srvRecords.add(new SrvRecord(parts[3], Integer.parseInt(parts[2])));
151         } catch (RuntimeException e) {
152           logger.log(level, "Failed to construct SRV record " + rawSrv, e);
153           if (first == null) {
154             first = e;
155             level = Level.FINE;
156           }
157         }
158       }
159       if (srvRecords.isEmpty() && first != null) {
160         throw first;
161       }
162       return Collections.unmodifiableList(srvRecords);
163     }
164 
165     /**
166      * Undo the quoting done in {@link com.sun.jndi.dns.ResourceRecord#decodeTxt}.
167      */
168     @VisibleForTesting
unquote(String txtRecord)169     static String unquote(String txtRecord) {
170       StringBuilder sb = new StringBuilder(txtRecord.length());
171       boolean inquote = false;
172       for (int i = 0; i < txtRecord.length(); i++) {
173         char c = txtRecord.charAt(i);
174         if (!inquote) {
175           if (c == ' ') {
176             continue;
177           } else if (c == '"') {
178             inquote = true;
179             continue;
180           }
181         } else {
182           if (c == '"') {
183             inquote = false;
184             continue;
185           } else if (c == '\\') {
186             c = txtRecord.charAt(++i);
187             assert c == '"' || c == '\\';
188           }
189         }
190         sb.append(c);
191       }
192       return sb.toString();
193     }
194   }
195 
196   @VisibleForTesting
197   @IgnoreJRERequirement
198   @SuppressWarnings({"JdkObsolete", "BanJNDI"})
199   // javax.naming.* is only loaded reflectively and is never loaded for Android
200   // The lint issue id is supposed to be "InvalidPackage" but it doesn't work, don't know why.
201   // Use "all" as the lint issue id to suppress all types of lint error.
202   @SuppressLint("all")
203   static final class JndiRecordFetcher implements RecordFetcher {
204     @Override
getAllRecords(String recordType, String name)205     public List<String> getAllRecords(String recordType, String name) throws NamingException {
206       checkAvailable();
207       String[] rrType = new String[]{recordType};
208       List<String> records = new ArrayList<>();
209 
210       Hashtable<String, String> env = new Hashtable<>();
211       env.put("com.sun.jndi.ldap.connect.timeout", "5000");
212       env.put("com.sun.jndi.ldap.read.timeout", "5000");
213       DirContext dirContext = new InitialDirContext(env);
214 
215       try {
216         javax.naming.directory.Attributes attrs = dirContext.getAttributes(name, rrType);
217         NamingEnumeration<? extends Attribute> rrGroups = attrs.getAll();
218 
219         try {
220           while (rrGroups.hasMore()) {
221             Attribute rrEntry = rrGroups.next();
222             assert Arrays.asList(rrType).contains(rrEntry.getID());
223             NamingEnumeration<?> rrValues = rrEntry.getAll();
224             try {
225               while (rrValues.hasMore()) {
226                 records.add(String.valueOf(rrValues.next()));
227               }
228             } catch (NamingException ne) {
229               closeThenThrow(rrValues, ne);
230             }
231             rrValues.close();
232           }
233         } catch (NamingException ne) {
234           closeThenThrow(rrGroups, ne);
235         }
236         rrGroups.close();
237       } catch (NamingException ne) {
238         closeThenThrow(dirContext, ne);
239       }
240       dirContext.close();
241 
242       return records;
243     }
244 
closeThenThrow(NamingEnumeration<?> namingEnumeration, NamingException e)245     private static void closeThenThrow(NamingEnumeration<?> namingEnumeration, NamingException e)
246         throws NamingException {
247       try {
248         namingEnumeration.close();
249       } catch (NamingException ignored) {
250         // ignore
251       }
252       throw e;
253     }
254 
closeThenThrow(DirContext ctx, NamingException e)255     private static void closeThenThrow(DirContext ctx, NamingException e) throws NamingException {
256       try {
257         ctx.close();
258       } catch (NamingException ignored) {
259         // ignore
260       }
261       throw e;
262     }
263 
checkAvailable()264     private static void checkAvailable() {
265       if (JNDI_UNAVAILABILITY_CAUSE != null) {
266         throw new UnsupportedOperationException(
267             "JNDI is not currently available", JNDI_UNAVAILABILITY_CAUSE);
268       }
269     }
270   }
271 }
272