001/** 002 * 003 * Copyright 2003-2005 Jive Software. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smack.util; 018 019import java.lang.reflect.InvocationTargetException; 020import java.lang.reflect.Method; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.SortedMap; 026import java.util.TreeMap; 027import java.util.logging.Level; 028import java.util.logging.Logger; 029 030import org.jivesoftware.smack.util.dns.DNSResolver; 031import org.jivesoftware.smack.util.dns.HostAddress; 032import org.jivesoftware.smack.util.dns.SRVRecord; 033 034/** 035 * Utility class to perform DNS lookups for XMPP services. 036 * 037 * @author Matt Tucker 038 */ 039public class DNSUtil { 040 041 private static final Logger LOGGER = Logger.getLogger(DNSUtil.class.getName()); 042 private static DNSResolver dnsResolver = null; 043 044 /** 045 * Initializes DNSUtil. This method is automatically called by SmackConfiguration, you don't 046 * have to call it manually. 047 */ 048 public static void init() { 049 final String[] RESOLVERS = new String[] { "javax.JavaxResolver", "minidns.MiniDnsResolver", 050 "dnsjava.DNSJavaResolver" }; 051 for (String resolver :RESOLVERS) { 052 DNSResolver availableResolver = null; 053 String resolverFull = "org.jivesoftware.smack.util.dns." + resolver; 054 try { 055 Class<?> resolverClass = Class.forName(resolverFull); 056 Method getInstanceMethod = resolverClass.getMethod("getInstance"); 057 availableResolver = (DNSResolver) getInstanceMethod.invoke(null); 058 if (availableResolver != null) { 059 setDNSResolver(availableResolver); 060 break; 061 } 062 } 063 catch (ClassNotFoundException|NoSuchMethodException|SecurityException|IllegalAccessException|IllegalArgumentException|InvocationTargetException e) { 064 LOGGER.log(Level.FINE, "Exception on init", e); 065 } 066 } 067 } 068 069 /** 070 * Set the DNS resolver that should be used to perform DNS lookups. 071 * 072 * @param resolver 073 */ 074 public static void setDNSResolver(DNSResolver resolver) { 075 dnsResolver = resolver; 076 } 077 078 /** 079 * Returns the current DNS resolved used to perform DNS lookups. 080 * 081 * @return the active DNSResolver 082 */ 083 public static DNSResolver getDNSResolver() { 084 return dnsResolver; 085 } 086 087 /** 088 * Returns a list of HostAddresses under which the specified XMPP server can be reached at for client-to-server 089 * communication. A DNS lookup for a SRV record in the form "_xmpp-client._tcp.example.com" is attempted, according 090 * to section 3.2.1 of RFC 6120. If that lookup fails, it's assumed that the XMPP server lives at the host resolved 091 * by a DNS lookup at the specified domain on the default port of 5222. 092 * <p> 093 * As an example, a lookup for "example.com" may return "im.example.com:5269". 094 * </p> 095 * 096 * @param domain the domain. 097 * @return List of HostAddress, which encompasses the hostname and port that the XMPP server can be reached at for 098 * the specified domain. 099 */ 100 public static List<HostAddress> resolveXMPPDomain(final String domain) { 101 if (dnsResolver == null) { 102 List<HostAddress> addresses = new ArrayList<HostAddress>(1); 103 addresses.add(new HostAddress(domain, 5222)); 104 return addresses; 105 } 106 return resolveDomain(domain, 'c'); 107 } 108 109 /** 110 * Returns a list of HostAddresses under which the specified XMPP server can be reached at for server-to-server 111 * communication. A DNS lookup for a SRV record in the form "_xmpp-server._tcp.example.com" is attempted, according 112 * to section 3.2.1 of RFC 6120. If that lookup fails , it's assumed that the XMPP server lives at the host resolved 113 * by a DNS lookup at the specified domain on the default port of 5269. 114 * <p> 115 * As an example, a lookup for "example.com" may return "im.example.com:5269". 116 * </p> 117 * 118 * @param domain the domain. 119 * @return List of HostAddress, which encompasses the hostname and port that the XMPP server can be reached at for 120 * the specified domain. 121 */ 122 public static List<HostAddress> resolveXMPPServerDomain(final String domain) { 123 if (dnsResolver == null) { 124 List<HostAddress> addresses = new ArrayList<HostAddress>(1); 125 addresses.add(new HostAddress(domain, 5269)); 126 return addresses; 127 } 128 return resolveDomain(domain, 's'); 129 } 130 131 private static List<HostAddress> resolveDomain(String domain, char keyPrefix) { 132 List<HostAddress> addresses = new ArrayList<HostAddress>(); 133 134 // Step one: Do SRV lookups 135 String srvDomain; 136 if (keyPrefix == 's') { 137 srvDomain = "_xmpp-server._tcp." + domain; 138 } else if (keyPrefix == 'c') { 139 srvDomain = "_xmpp-client._tcp." + domain; 140 } else { 141 srvDomain = domain; 142 } 143 try { 144 List<SRVRecord> srvRecords = dnsResolver.lookupSRVRecords(srvDomain); 145 if (LOGGER.isLoggable(Level.FINE)) { 146 String logMessage = "Resolved SRV RR for " + srvDomain + ":"; 147 for (SRVRecord r : srvRecords) 148 logMessage += " " + r; 149 LOGGER.fine(logMessage); 150 } 151 List<HostAddress> sortedRecords = sortSRVRecords(srvRecords); 152 addresses.addAll(sortedRecords); 153 } 154 catch (Exception e) { 155 LOGGER.log(Level.WARNING, "Exception while resovling SRV records for " + domain 156 + ". Consider adding '_xmpp-(server|client)._tcp' DNS SRV Records"); 157 } 158 159 // Step two: Add the hostname to the end of the list 160 addresses.add(new HostAddress(domain)); 161 162 return addresses; 163 } 164 165 /** 166 * Sort a given list of SRVRecords as described in RFC 2782 167 * Note that we follow the RFC with one exception. In a group of the same priority, only the first entry 168 * is calculated by random. The others are ore simply ordered by their priority. 169 * 170 * @param records 171 * @return the list of resolved HostAddresses 172 */ 173 private static List<HostAddress> sortSRVRecords(List<SRVRecord> records) { 174 // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "." 175 // (the root domain), abort." 176 if (records.size() == 1 && records.get(0).getFQDN().equals(".")) 177 return Collections.emptyList(); 178 179 // sorting the records improves the performance of the bisection later 180 Collections.sort(records); 181 182 // create the priority buckets 183 SortedMap<Integer, List<SRVRecord>> buckets = new TreeMap<Integer, List<SRVRecord>>(); 184 for (SRVRecord r : records) { 185 Integer priority = r.getPriority(); 186 List<SRVRecord> bucket = buckets.get(priority); 187 // create the list of SRVRecords if it doesn't exist 188 if (bucket == null) { 189 bucket = new LinkedList<SRVRecord>(); 190 buckets.put(priority, bucket); 191 } 192 bucket.add(r); 193 } 194 195 List<HostAddress> res = new ArrayList<HostAddress>(records.size()); 196 197 for (Integer priority : buckets.keySet()) { 198 List<SRVRecord> bucket = buckets.get(priority); 199 int bucketSize; 200 while ((bucketSize = bucket.size()) > 0) { 201 int[] totals = new int[bucket.size()]; 202 int running_total = 0; 203 int count = 0; 204 int zeroWeight = 1; 205 206 for (SRVRecord r : bucket) { 207 if (r.getWeight() > 0) 208 zeroWeight = 0; 209 } 210 211 for (SRVRecord r : bucket) { 212 running_total += (r.getWeight() + zeroWeight); 213 totals[count] = running_total; 214 count++; 215 } 216 int selectedPos; 217 if (running_total == 0) { 218 // If running total is 0, then all weights in this priority 219 // group are 0. So we simply select one of the weights randomly 220 // as the other 'normal' algorithm is unable to handle this case 221 selectedPos = (int) (Math.random() * bucketSize); 222 } else { 223 double rnd = Math.random() * running_total; 224 selectedPos = bisect(totals, rnd); 225 } 226 // add the SRVRecord that was randomly chosen on it's weight 227 // to the start of the result list 228 SRVRecord chosenSRVRecord = bucket.remove(selectedPos); 229 res.add(chosenSRVRecord); 230 } 231 } 232 233 return res; 234 } 235 236 // TODO this is not yet really bisection just a stupid linear search 237 private static int bisect(int[] array, double value) { 238 int pos = 0; 239 for (int element : array) { 240 if (value < element) 241 break; 242 pos++; 243 } 244 return pos; 245 } 246}