DNSUtil.java

  1. /**
  2.  *
  3.  * Copyright 2003-2005 Jive Software, 2016-2018 Florian Schmaus.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smack.util;

  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.LinkedList;
  21. import java.util.List;
  22. import java.util.SortedMap;
  23. import java.util.TreeMap;
  24. import java.util.logging.Level;
  25. import java.util.logging.Logger;

  26. import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode;
  27. import org.jivesoftware.smack.util.dns.DNSResolver;
  28. import org.jivesoftware.smack.util.dns.HostAddress;
  29. import org.jivesoftware.smack.util.dns.SRVRecord;
  30. import org.jivesoftware.smack.util.dns.SmackDaneProvider;

  31. import org.minidns.dnsname.DnsName;

  32. /**
  33.  * Utility class to perform DNS lookups for XMPP services.
  34.  *
  35.  * @author Matt Tucker
  36.  * @author Florian Schmaus
  37.  */
  38. public class DNSUtil {

  39.     public static final String XMPP_CLIENT_DNS_SRV_PREFIX = "_xmpp-client._tcp";
  40.     public static final String XMPP_SERVER_DNS_SRV_PREFIX = "_xmpp-server._tcp";

  41.     private static final Logger LOGGER = Logger.getLogger(DNSUtil.class.getName());
  42.     private static DNSResolver dnsResolver = null;
  43.     private static SmackDaneProvider daneProvider;

  44.     /**
  45.      * Set the DNS resolver that should be used to perform DNS lookups.
  46.      *
  47.      * @param resolver
  48.      */
  49.     public static void setDNSResolver(DNSResolver resolver) {
  50.         dnsResolver = Objects.requireNonNull(resolver);
  51.     }

  52.     /**
  53.      * Returns the current DNS resolved used to perform DNS lookups.
  54.      *
  55.      * @return the active DNSResolver
  56.      */
  57.     public static DNSResolver getDNSResolver() {
  58.         return dnsResolver;
  59.     }

  60.     /**
  61.      * Set the DANE provider that should be used when DANE is enabled.
  62.      *
  63.      * @param daneProvider
  64.      */
  65.     public static void setDaneProvider(SmackDaneProvider daneProvider) {
  66.         DNSUtil.daneProvider = Objects.requireNonNull(daneProvider);
  67.     }

  68.     /**
  69.      * Returns the currently active DANE provider used when DANE is enabled.
  70.      *
  71.      * @return the active DANE provider
  72.      */
  73.     public static SmackDaneProvider getDaneProvider() {
  74.         return daneProvider;
  75.     }

  76.     @SuppressWarnings("ImmutableEnumChecker")
  77.     enum DomainType {
  78.         server(XMPP_SERVER_DNS_SRV_PREFIX),
  79.         client(XMPP_CLIENT_DNS_SRV_PREFIX),
  80.         ;
  81.         public final DnsName srvPrefix;

  82.         DomainType(String srvPrefixString) {
  83.             srvPrefix = DnsName.from(srvPrefixString);
  84.         }
  85.     }

  86.     /**
  87.      * Returns a list of HostAddresses under which the specified XMPP server can be reached at for client-to-server
  88.      * communication. A DNS lookup for a SRV record in the form "_xmpp-client._tcp.example.com" is attempted, according
  89.      * to section 3.2.1 of RFC 6120. If that lookup fails, it's assumed that the XMPP server lives at the host resolved
  90.      * by a DNS lookup at the specified domain on the default port of 5222.
  91.      * <p>
  92.      * As an example, a lookup for "example.com" may return "im.example.com:5269".
  93.      * </p>
  94.      *
  95.      * @param domain the domain.
  96.      * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve.
  97.      * @param dnssecMode DNSSec mode.
  98.      * @return List of HostAddress, which encompasses the hostname and port that the
  99.      *      XMPP server can be reached at for the specified domain.
  100.      */
  101.     public static List<HostAddress> resolveXMPPServiceDomain(DnsName domain, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
  102.         return resolveDomain(domain, DomainType.client, failedAddresses, dnssecMode);
  103.     }

  104.     /**
  105.      * Returns a list of HostAddresses under which the specified XMPP server can be reached at for server-to-server
  106.      * communication. A DNS lookup for a SRV record in the form "_xmpp-server._tcp.example.com" is attempted, according
  107.      * to section 3.2.1 of RFC 6120. If that lookup fails , it's assumed that the XMPP server lives at the host resolved
  108.      * by a DNS lookup at the specified domain on the default port of 5269.
  109.      * <p>
  110.      * As an example, a lookup for "example.com" may return "im.example.com:5269".
  111.      * </p>
  112.      *
  113.      * @param domain the domain.
  114.      * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve.
  115.      * @param dnssecMode DNSSec mode.
  116.      * @return List of HostAddress, which encompasses the hostname and port that the
  117.      *      XMPP server can be reached at for the specified domain.
  118.      */
  119.     public static List<HostAddress> resolveXMPPServerDomain(DnsName domain, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
  120.         return resolveDomain(domain, DomainType.server, failedAddresses, dnssecMode);
  121.     }

  122.     /**
  123.      *
  124.      * @param domain the domain.
  125.      * @param domainType the XMPP domain type, server or client.
  126.      * @param failedAddresses a list that will be populated with host addresses that failed to resolve.
  127.      * @return a list of resolver host addresses for this domain.
  128.      */
  129.     private static List<HostAddress> resolveDomain(DnsName domain, DomainType domainType,
  130.                     List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
  131.         if (dnsResolver == null) {
  132.             throw new IllegalStateException("No DNS Resolver active in Smack");
  133.         }

  134.         List<HostAddress> addresses = new ArrayList<HostAddress>();

  135.         // Step one: Do SRV lookups
  136.         DnsName srvDomain = DnsName.from(domainType.srvPrefix, domain);

  137.         List<SRVRecord> srvRecords = dnsResolver.lookupSRVRecords(srvDomain, failedAddresses, dnssecMode);
  138.         if (srvRecords != null && !srvRecords.isEmpty()) {
  139.             if (LOGGER.isLoggable(Level.FINE)) {
  140.                 String logMessage = "Resolved SRV RR for " + srvDomain + ":";
  141.                 for (SRVRecord r : srvRecords)
  142.                     logMessage += " " + r;
  143.                 LOGGER.fine(logMessage);
  144.             }
  145.             List<HostAddress> sortedRecords = sortSRVRecords(srvRecords);
  146.             addresses.addAll(sortedRecords);
  147.         } else {
  148.             LOGGER.info("Could not resolve DNS SRV resource records for " + srvDomain + ". Consider adding those.");
  149.         }

  150.         int defaultPort = -1;
  151.         switch (domainType) {
  152.         case client:
  153.             defaultPort = 5222;
  154.             break;
  155.         case server:
  156.             defaultPort = 5269;
  157.             break;
  158.         }
  159.         // Step two: Add the hostname to the end of the list
  160.         HostAddress hostAddress = dnsResolver.lookupHostAddress(domain, defaultPort, failedAddresses, dnssecMode);
  161.         if (hostAddress != null) {
  162.             addresses.add(hostAddress);
  163.         }

  164.         return addresses;
  165.     }

  166.     /**
  167.      * Sort a given list of SRVRecords as described in RFC 2782
  168.      * Note that we follow the RFC with one exception. In a group of the same priority, only the first entry
  169.      * is calculated by random. The others are ore simply ordered by their priority.
  170.      *
  171.      * @param records
  172.      * @return the list of resolved HostAddresses
  173.      */
  174.     private static List<HostAddress> sortSRVRecords(List<SRVRecord> records) {
  175.         // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "."
  176.         // (the root domain), abort."
  177.         if (records.size() == 1 && records.get(0).getFQDN().isRootLabel())
  178.             return Collections.emptyList();

  179.         // sorting the records improves the performance of the bisection later
  180.         Collections.sort(records);

  181.         // create the priority buckets
  182.         SortedMap<Integer, List<SRVRecord>> buckets = new TreeMap<Integer, List<SRVRecord>>();
  183.         for (SRVRecord r : records) {
  184.             Integer priority = r.getPriority();
  185.             List<SRVRecord> bucket = buckets.get(priority);
  186.             // create the list of SRVRecords if it doesn't exist
  187.             if (bucket == null) {
  188.                 bucket = new LinkedList<SRVRecord>();
  189.                 buckets.put(priority, bucket);
  190.             }
  191.             bucket.add(r);
  192.         }

  193.         List<HostAddress> res = new ArrayList<HostAddress>(records.size());

  194.         for (Integer priority : buckets.keySet()) {
  195.             List<SRVRecord> bucket = buckets.get(priority);
  196.             int bucketSize;
  197.             while ((bucketSize = bucket.size()) > 0) {
  198.                 int[] totals = new int[bucketSize];
  199.                 int running_total = 0;
  200.                 int count = 0;
  201.                 int zeroWeight = 1;

  202.                 for (SRVRecord r : bucket) {
  203.                     if (r.getWeight() > 0) {
  204.                         zeroWeight = 0;
  205.                         break;
  206.                     }
  207.                 }

  208.                 for (SRVRecord r : bucket) {
  209.                     running_total += (r.getWeight() + zeroWeight);
  210.                     totals[count] = running_total;
  211.                     count++;
  212.                 }
  213.                 int selectedPos;
  214.                 if (running_total == 0) {
  215.                     // If running total is 0, then all weights in this priority
  216.                     // group are 0. So we simply select one of the weights randomly
  217.                     // as the other 'normal' algorithm is unable to handle this case
  218.                     selectedPos = (int) (Math.random() * bucketSize);
  219.                 } else {
  220.                     double rnd = Math.random() * running_total;
  221.                     selectedPos = bisect(totals, rnd);
  222.                 }
  223.                 // add the SRVRecord that was randomly chosen on it's weight
  224.                 // to the start of the result list
  225.                 SRVRecord chosenSRVRecord = bucket.remove(selectedPos);
  226.                 res.add(chosenSRVRecord);
  227.             }
  228.         }

  229.         return res;
  230.     }

  231.     // TODO this is not yet really bisection just a stupid linear search
  232.     private static int bisect(int[] array, double value) {
  233.         int pos = 0;
  234.         for (int element : array) {
  235.             if (value < element)
  236.                 break;
  237.             pos++;
  238.         }
  239.         return pos;
  240.     }

  241. }