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