DNSUtil.java

  1. /**
  2.  *
  3.  * Copyright 2003-2005 Jive Software.
  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.util.dns.DNSResolver;
  27. import org.jivesoftware.smack.util.dns.HostAddress;
  28. import org.jivesoftware.smack.util.dns.SRVRecord;

  29. /**
  30.  * Utility class to perform DNS lookups for XMPP services.
  31.  *
  32.  * @author Matt Tucker
  33.  */
  34. public class DNSUtil {

  35.     private static final Logger LOGGER = Logger.getLogger(DNSUtil.class.getName());
  36.     private static DNSResolver dnsResolver = null;

  37.     /**
  38.      * International Domain Name transformer.
  39.      * <p>
  40.      * Used to transform Unicode representations of the Domain Name to ASCII in
  41.      * order to perform a DNS request with the ASCII representation.
  42.      * 'java.net.IDN' is available since Android API 9, but as long as Smack
  43.      * requires API 8, we are going to need this. This part is going to get
  44.      * removed once Smack depends on Android API 9 or higher.
  45.      * </p>
  46.      */
  47.     private static StringTransformer idnaTransformer = new StringTransformer() {
  48.         @Override
  49.         public String transform(String string) {
  50.             return string;
  51.         }
  52.     };

  53.     /**
  54.      * Set the DNS resolver that should be used to perform DNS lookups.
  55.      *
  56.      * @param resolver
  57.      */
  58.     public static void setDNSResolver(DNSResolver resolver) {
  59.         dnsResolver = resolver;
  60.     }

  61.     /**
  62.      * Returns the current DNS resolved used to perform DNS lookups.
  63.      *
  64.      * @return the active DNSResolver
  65.      */
  66.     public static DNSResolver getDNSResolver() {
  67.         return dnsResolver;
  68.     }


  69.     /**
  70.      * Set the IDNA (Internationalizing Domain Names in Applications, RFC 3490) transformer.
  71.      * <p>
  72.      * You usually want to wrap 'java.net.IDN.toASCII()' into a StringTransformer here.
  73.      * </p>
  74.      * @param idnaTransformer
  75.      */
  76.     public static void setIdnaTransformer(StringTransformer idnaTransformer) {
  77.         if (idnaTransformer == null) {
  78.             throw new NullPointerException();
  79.         }
  80.         DNSUtil.idnaTransformer = idnaTransformer;
  81.     }

  82.     private static enum DomainType {
  83.         Server,
  84.         Client,
  85.         ;
  86.     }

  87.     /**
  88.      * Returns a list of HostAddresses under which the specified XMPP server can be reached at for client-to-server
  89.      * communication. A DNS lookup for a SRV record in the form "_xmpp-client._tcp.example.com" is attempted, according
  90.      * to section 3.2.1 of RFC 6120. If that lookup fails, it's assumed that the XMPP server lives at the host resolved
  91.      * by a DNS lookup at the specified domain on the default port of 5222.
  92.      * <p>
  93.      * As an example, a lookup for "example.com" may return "im.example.com:5269".
  94.      * </p>
  95.      *
  96.      * @param domain the domain.
  97.      * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve.
  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> resolveXMPPDomain(String domain, List<HostAddress> failedAddresses) {
  102.         domain = idnaTransformer.transform(domain);
  103.         if (dnsResolver == null) {
  104.             LOGGER.warning("No DNS Resolver active in Smack, will be unable to perform DNS SRV lookups");
  105.             List<HostAddress> addresses = new ArrayList<HostAddress>(1);
  106.             addresses.add(new HostAddress(domain, 5222));
  107.             return addresses;
  108.         }
  109.         return resolveDomain(domain, DomainType.Client, failedAddresses);
  110.     }

  111.     /**
  112.      * Returns a list of HostAddresses under which the specified XMPP server can be reached at for server-to-server
  113.      * communication. A DNS lookup for a SRV record in the form "_xmpp-server._tcp.example.com" is attempted, according
  114.      * to section 3.2.1 of RFC 6120. If that lookup fails , it's assumed that the XMPP server lives at the host resolved
  115.      * by a DNS lookup at the specified domain on the default port of 5269.
  116.      * <p>
  117.      * As an example, a lookup for "example.com" may return "im.example.com:5269".
  118.      * </p>
  119.      *
  120.      * @param domain the domain.
  121.      * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve.
  122.      * @return List of HostAddress, which encompasses the hostname and port that the
  123.      *      XMPP server can be reached at for the specified domain.
  124.      */
  125.     public static List<HostAddress> resolveXMPPServerDomain(String domain, List<HostAddress> failedAddresses) {
  126.         domain = idnaTransformer.transform(domain);
  127.         if (dnsResolver == null) {
  128.             LOGGER.warning("No DNS Resolver active in Smack, will be unable to perform DNS SRV lookups");
  129.             List<HostAddress> addresses = new ArrayList<HostAddress>(1);
  130.             addresses.add(new HostAddress(domain, 5269));
  131.             return addresses;
  132.         }
  133.         return resolveDomain(domain, DomainType.Server, failedAddresses);
  134.     }

  135.     /**
  136.      *
  137.      * @param domain the domain.
  138.      * @param domainType the XMPP domain type, server or client.
  139.      * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve.
  140.      * @return a list of resolver host addresses for this domain.
  141.      */
  142.     private static List<HostAddress> resolveDomain(String domain, DomainType domainType, List<HostAddress> failedAddresses) {
  143.         List<HostAddress> addresses = new ArrayList<HostAddress>();

  144.         // Step one: Do SRV lookups
  145.         String srvDomain;
  146.         switch (domainType) {
  147.         case Server:
  148.             srvDomain = "_xmpp-server._tcp." + domain;
  149.             break;
  150.         case Client:
  151.             srvDomain = "_xmpp-client._tcp." + domain;
  152.             break;
  153.         default:
  154.             throw new AssertionError();
  155.         }
  156.         try {
  157.             List<SRVRecord> srvRecords = dnsResolver.lookupSRVRecords(srvDomain);
  158.             if (LOGGER.isLoggable(Level.FINE)) {
  159.                 String logMessage = "Resolved SRV RR for " + srvDomain + ":";
  160.                 for (SRVRecord r : srvRecords)
  161.                     logMessage += " " + r;
  162.                 LOGGER.fine(logMessage);
  163.             }
  164.             List<HostAddress> sortedRecords = sortSRVRecords(srvRecords);
  165.             addresses.addAll(sortedRecords);
  166.         }
  167.         catch (Exception e) {
  168.             LOGGER.log(Level.WARNING, "Exception while resovling SRV records for " + domain
  169.                             + ". Consider adding '_xmpp-(server|client)._tcp' DNS SRV Records", e);
  170.             if (failedAddresses != null) {
  171.                 HostAddress failedHostAddress = new HostAddress(srvDomain);
  172.                 failedHostAddress.setException(e);
  173.                 failedAddresses.add(failedHostAddress);
  174.             }
  175.         }

  176.         // Step two: Add the hostname to the end of the list
  177.         addresses.add(new HostAddress(domain));

  178.         return addresses;
  179.     }

  180.     /**
  181.      * Sort a given list of SRVRecords as described in RFC 2782
  182.      * Note that we follow the RFC with one exception. In a group of the same priority, only the first entry
  183.      * is calculated by random. The others are ore simply ordered by their priority.
  184.      *
  185.      * @param records
  186.      * @return the list of resolved HostAddresses
  187.      */
  188.     private static List<HostAddress> sortSRVRecords(List<SRVRecord> records) {
  189.         // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "."
  190.         // (the root domain), abort."
  191.         if (records.size() == 1 && records.get(0).getFQDN().equals("."))
  192.             return Collections.emptyList();

  193.         // sorting the records improves the performance of the bisection later
  194.         Collections.sort(records);

  195.         // create the priority buckets
  196.         SortedMap<Integer, List<SRVRecord>> buckets = new TreeMap<Integer, List<SRVRecord>>();
  197.         for (SRVRecord r : records) {
  198.             Integer priority = r.getPriority();
  199.             List<SRVRecord> bucket = buckets.get(priority);
  200.             // create the list of SRVRecords if it doesn't exist
  201.             if (bucket == null) {
  202.                 bucket = new LinkedList<SRVRecord>();
  203.                 buckets.put(priority, bucket);
  204.             }
  205.             bucket.add(r);
  206.         }

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

  208.         for (Integer priority : buckets.keySet()) {
  209.             List<SRVRecord> bucket = buckets.get(priority);
  210.             int bucketSize;
  211.             while ((bucketSize = bucket.size()) > 0) {
  212.                 int[] totals = new int[bucket.size()];
  213.                 int running_total = 0;
  214.                 int count = 0;
  215.                 int zeroWeight = 1;

  216.                 for (SRVRecord r : bucket) {
  217.                     if (r.getWeight() > 0)
  218.                         zeroWeight = 0;
  219.                 }

  220.                 for (SRVRecord r : bucket) {
  221.                     running_total += (r.getWeight() + zeroWeight);
  222.                     totals[count] = running_total;
  223.                     count++;
  224.                 }
  225.                 int selectedPos;
  226.                 if (running_total == 0) {
  227.                     // If running total is 0, then all weights in this priority
  228.                     // group are 0. So we simply select one of the weights randomly
  229.                     // as the other 'normal' algorithm is unable to handle this case
  230.                     selectedPos = (int) (Math.random() * bucketSize);
  231.                 } else {
  232.                     double rnd = Math.random() * running_total;
  233.                     selectedPos = bisect(totals, rnd);
  234.                 }
  235.                 // add the SRVRecord that was randomly chosen on it's weight
  236.                 // to the start of the result list
  237.                 SRVRecord chosenSRVRecord = bucket.remove(selectedPos);
  238.                 res.add(chosenSRVRecord);
  239.             }
  240.         }

  241.         return res;
  242.     }

  243.     // TODO this is not yet really bisection just a stupid linear search
  244.     private static int bisect(int[] array, double value) {
  245.         int pos = 0;
  246.         for (int element : array) {
  247.             if (value < element)
  248.                 break;
  249.             pos++;
  250.         }
  251.         return pos;
  252.     }

  253. }