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