blob: b095266a0469f69522122890539160ddc3c507ff [file] [log] [blame]
/*
* Copyright 2018 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.internal;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Verify;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.internal.DnsNameResolver.AddressResolver;
import io.grpc.internal.DnsNameResolver.ResourceResolver;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
/**
* {@link JndiResourceResolverFactory} resolves additional records for the DnsNameResolver.
*/
final class JndiResourceResolverFactory implements DnsNameResolver.ResourceResolverFactory {
@Nullable
private static final Throwable JNDI_UNAVAILABILITY_CAUSE = initJndi();
// @UsedReflectively
public JndiResourceResolverFactory() {}
/**
* Returns whether the JNDI DNS resolver is available. This is accomplished by looking up a
* particular class. It is believed to be the default (only?) DNS resolver that will actually be
* used. It is provided by the OpenJDK, but unlikely Android. Actual resolution will be done by
* using a service provider when a hostname query is present, so the {@code DnsContextFactory}
* may not actually be used to perform the query. This is believed to be "okay."
*/
@Nullable
@SuppressWarnings("LiteralClassName")
private static Throwable initJndi() {
if (GrpcUtil.IS_RESTRICTED_APPENGINE) {
return new UnsupportedOperationException(
"Currently running in an AppEngine restricted environment");
}
try {
Class.forName("javax.naming.directory.InitialDirContext");
Class.forName("com.sun.jndi.dns.DnsContextFactory");
} catch (ClassNotFoundException e) {
return e;
} catch (RuntimeException e) {
return e;
} catch (Error e) {
return e;
}
return null;
}
@Nullable
@Override
public ResourceResolver newResourceResolver() {
if (unavailabilityCause() != null) {
return null;
}
return new JndiResourceResolver();
}
@Nullable
@Override
public Throwable unavailabilityCause() {
return JNDI_UNAVAILABILITY_CAUSE;
}
@VisibleForTesting
static final class JndiResourceResolver implements DnsNameResolver.ResourceResolver {
private static final Logger logger =
Logger.getLogger(JndiResourceResolver.class.getName());
private static final Pattern whitespace = Pattern.compile("\\s+");
@Override
public List<String> resolveTxt(String serviceConfigHostname) throws NamingException {
checkAvailable();
if (logger.isLoggable(Level.FINER)) {
logger.log(
Level.FINER, "About to query TXT records for {0}", new Object[]{serviceConfigHostname});
}
List<String> serviceConfigRawTxtRecords =
getAllRecords("TXT", "dns:///" + serviceConfigHostname);
if (logger.isLoggable(Level.FINER)) {
logger.log(
Level.FINER, "Found {0} TXT records", new Object[]{serviceConfigRawTxtRecords.size()});
}
List<String> serviceConfigTxtRecords =
new ArrayList<>(serviceConfigRawTxtRecords.size());
for (String serviceConfigRawTxtRecord : serviceConfigRawTxtRecords) {
serviceConfigTxtRecords.add(unquote(serviceConfigRawTxtRecord));
}
return Collections.unmodifiableList(serviceConfigTxtRecords);
}
@Override
public List<EquivalentAddressGroup> resolveSrv(
AddressResolver addressResolver, String grpclbHostname) throws Exception {
checkAvailable();
if (logger.isLoggable(Level.FINER)) {
logger.log(
Level.FINER, "About to query SRV records for {0}", new Object[]{grpclbHostname});
}
List<String> grpclbSrvRecords =
getAllRecords("SRV", "dns:///" + grpclbHostname);
if (logger.isLoggable(Level.FINER)) {
logger.log(
Level.FINER, "Found {0} SRV records", new Object[]{grpclbSrvRecords.size()});
}
List<EquivalentAddressGroup> balancerAddresses =
new ArrayList<>(grpclbSrvRecords.size());
Exception first = null;
Level level = Level.WARNING;
for (String srvRecord : grpclbSrvRecords) {
try {
SrvRecord record = parseSrvRecord(srvRecord);
List<? extends InetAddress> addrs = addressResolver.resolveAddress(record.host);
List<SocketAddress> sockaddrs = new ArrayList<>(addrs.size());
for (InetAddress addr : addrs) {
sockaddrs.add(new InetSocketAddress(addr, record.port));
}
Attributes attrs = Attributes.newBuilder()
.set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, record.host)
.build();
balancerAddresses.add(
new EquivalentAddressGroup(Collections.unmodifiableList(sockaddrs), attrs));
} catch (UnknownHostException e) {
logger.log(level, "Can't find address for SRV record " + srvRecord, e);
// TODO(carl-mastrangelo): these should be added by addSuppressed when we have Java 7.
if (first == null) {
first = e;
level = Level.FINE;
}
} catch (RuntimeException e) {
logger.log(level, "Failed to construct SRV record " + srvRecord, e);
if (first == null) {
first = e;
level = Level.FINE;
}
}
}
if (balancerAddresses.isEmpty() && first != null) {
throw first;
}
return Collections.unmodifiableList(balancerAddresses);
}
@VisibleForTesting
static final class SrvRecord {
SrvRecord(String host, int port) {
this.host = host;
this.port = port;
}
final String host;
final int port;
}
@VisibleForTesting
@SuppressWarnings("BetaApi") // Verify is only kinda beta
static SrvRecord parseSrvRecord(String rawRecord) {
String[] parts = whitespace.split(rawRecord);
Verify.verify(parts.length == 4, "Bad SRV Record: %s", rawRecord);
return new SrvRecord(parts[3], Integer.parseInt(parts[2]));
}
@IgnoreJRERequirement
private static List<String> getAllRecords(String recordType, String name)
throws NamingException {
String[] rrType = new String[]{recordType};
List<String> records = new ArrayList<>();
@SuppressWarnings("JdkObsolete")
Hashtable<String, String> env = new Hashtable<String, String>();
env.put("com.sun.jndi.ldap.connect.timeout", "5000");
env.put("com.sun.jndi.ldap.read.timeout", "5000");
DirContext dirContext = new InitialDirContext(env);
try {
javax.naming.directory.Attributes attrs = dirContext.getAttributes(name, rrType);
NamingEnumeration<? extends Attribute> rrGroups = attrs.getAll();
try {
while (rrGroups.hasMore()) {
Attribute rrEntry = rrGroups.next();
assert Arrays.asList(rrType).contains(rrEntry.getID());
NamingEnumeration<?> rrValues = rrEntry.getAll();
try {
while (rrValues.hasMore()) {
records.add(String.valueOf(rrValues.next()));
}
} catch (NamingException ne) {
closeThenThrow(rrValues, ne);
}
rrValues.close();
}
} catch (NamingException ne) {
closeThenThrow(rrGroups, ne);
}
rrGroups.close();
} catch (NamingException ne) {
closeThenThrow(dirContext, ne);
}
dirContext.close();
return records;
}
@IgnoreJRERequirement
private static void closeThenThrow(NamingEnumeration<?> namingEnumeration, NamingException e)
throws NamingException {
try {
namingEnumeration.close();
} catch (NamingException ignored) {
// ignore
}
throw e;
}
@IgnoreJRERequirement
private static void closeThenThrow(DirContext ctx, NamingException e) throws NamingException {
try {
ctx.close();
} catch (NamingException ignored) {
// ignore
}
throw e;
}
/**
* Undo the quoting done in {@link com.sun.jndi.dns.ResourceRecord#decodeTxt}.
*/
@VisibleForTesting
static String unquote(String txtRecord) {
StringBuilder sb = new StringBuilder(txtRecord.length());
boolean inquote = false;
for (int i = 0; i < txtRecord.length(); i++) {
char c = txtRecord.charAt(i);
if (!inquote) {
if (c == ' ') {
continue;
} else if (c == '"') {
inquote = true;
continue;
}
} else {
if (c == '"') {
inquote = false;
continue;
} else if (c == '\\') {
c = txtRecord.charAt(++i);
assert c == '"' || c == '\\';
}
}
sb.append(c);
}
return sb.toString();
}
private static void checkAvailable() {
if (JNDI_UNAVAILABILITY_CAUSE != null) {
throw new UnsupportedOperationException(
"JNDI is not currently available", JNDI_UNAVAILABILITY_CAUSE);
}
}
}
}