/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * 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 com.mongodb.internal.dns;

import com.mongodb.MongoClientException;
import com.mongodb.MongoConfigurationException;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import static java.lang.String.format;
import static java.util.Arrays.asList;

Utility class for resolving SRV and TXT records.

This class should not be considered a part of the public API.

/** * Utility class for resolving SRV and TXT records. * * <p>This class should not be considered a part of the public API.</p> */
public final class DefaultDnsResolver implements DnsResolver { /* The format of SRV record is priority weight port target. e.g. 0 5 5060 example.com. The priority and weight are ignored, and we just concatenate the host (after removing the ending '.') and port with a ':' in between, as expected by ServerAddress. It's required that the srvHost has at least three parts (e.g. foo.bar.baz) and that all of the resolved hosts have a parent domain equal to the domain of the srvHost. */ @Override public List<String> resolveHostFromSrvRecords(final String srvHost) { String srvHostDomain = srvHost.substring(srvHost.indexOf('.') + 1); List<String> srvHostDomainParts = asList(srvHostDomain.split("\\.")); List<String> hosts = new ArrayList<String>(); InitialDirContext dirContext = createDnsDirContext(); try { Attributes attributes = dirContext.getAttributes("_mongodb._tcp." + srvHost, new String[]{"SRV"}); Attribute attribute = attributes.get("SRV"); if (attribute == null) { throw new MongoConfigurationException("No SRV records available for host " + srvHost); } NamingEnumeration<?> srvRecordEnumeration = attribute.getAll(); while (srvRecordEnumeration.hasMore()) { String srvRecord = (String) srvRecordEnumeration.next(); String[] split = srvRecord.split(" "); String resolvedHost = split[3].endsWith(".") ? split[3].substring(0, split[3].length() - 1) : split[3]; String resolvedHostDomain = resolvedHost.substring(resolvedHost.indexOf('.') + 1); if (!sameParentDomain(srvHostDomainParts, resolvedHostDomain)) { throw new MongoConfigurationException( format("The SRV host name '%s'resolved to a host '%s 'that is not in a sub-domain of the SRV host.", srvHost, resolvedHost)); } hosts.add(resolvedHost + ":" + split[2]); } if (hosts.isEmpty()) { throw new MongoConfigurationException("Unable to find any SRV records for host " + srvHost); } } catch (NamingException e) { throw new MongoConfigurationException("Unable to look up SRV record for host " + srvHost, e); } finally { try { dirContext.close(); } catch (NamingException e) { // ignore } } return hosts; } private static boolean sameParentDomain(final List<String> srvHostDomainParts, final String resolvedHostDomain) { List<String> resolvedHostDomainParts = asList(resolvedHostDomain.split("\\.")); if (srvHostDomainParts.size() > resolvedHostDomainParts.size()) { return false; } return resolvedHostDomainParts.subList(resolvedHostDomainParts.size() - srvHostDomainParts.size(), resolvedHostDomainParts.size()) .equals(srvHostDomainParts); } /* A TXT record is just a string We require each to be one or more query parameters for a MongoDB connection string. Here we concatenate TXT records together with a '&' separator as required by connection strings */ @Override public String resolveAdditionalQueryParametersFromTxtRecords(final String host) { String additionalQueryParameters = ""; InitialDirContext dirContext = createDnsDirContext(); try { Attributes attributes = dirContext.getAttributes(host, new String[]{"TXT"}); Attribute attribute = attributes.get("TXT"); if (attribute != null) { NamingEnumeration<?> txtRecordEnumeration = attribute.getAll(); if (txtRecordEnumeration.hasMore()) { // Remove all space characters, as the DNS resolver for TXT records inserts a space character // between each character-string in a single TXT record. That whitespace is spurious in // this context and must be removed additionalQueryParameters = ((String) txtRecordEnumeration.next()).replaceAll("\\s", ""); if (txtRecordEnumeration.hasMore()) { throw new MongoConfigurationException(format("Multiple TXT records found for host '%s'. Only one is permitted", host)); } } } } catch (NamingException e) { throw new MongoConfigurationException("Unable to look up TXT record for host " + host, e); } finally { try { dirContext.close(); } catch (NamingException e) { // ignore } } return additionalQueryParameters; } /* It's unfortunate that we take a runtime dependency on com.sun.jndi.dns.DnsContextFactory. This is not guaranteed to work on all JVMs but in practice is expected to work on most. */ private static InitialDirContext createDnsDirContext() { Hashtable<String, String> envProps = new Hashtable<String, String>(); envProps.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); try { return new InitialDirContext(envProps); } catch (NamingException e) { throw new MongoClientException("Unable to support mongodb+srv// style connections as the 'com.sun.jndi.dns.DnsContextFactory' " + "class is not available in this JRE. A JNDI context is required for resolving SRV records.", e); } } }