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