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.util.ArrayList; 020import java.util.Collections; 021import java.util.LinkedList; 022import java.util.List; 023import java.util.SortedMap; 024import java.util.TreeMap; 025import java.util.logging.Level; 026import java.util.logging.Logger; 027 028import org.jivesoftware.smack.util.dns.DNSResolver; 029import org.jivesoftware.smack.util.dns.HostAddress; 030import org.jivesoftware.smack.util.dns.SRVRecord; 031 032/** 033 * Utility class to perform DNS lookups for XMPP services. 034 * 035 * @author Matt Tucker 036 */ 037public class DNSUtil { 038 039 private static final Logger LOGGER = Logger.getLogger(DNSUtil.class.getName()); 040 private static DNSResolver dnsResolver = null; 041 042 /** 043 * International Domain Name transformer. 044 * <p> 045 * Used to transform Unicode representations of the Domain Name to ASCII in 046 * order to perform a DNS request with the ASCII representation. 047 * 'java.net.IDN' is available since Android API 9, but as long as Smack 048 * requires API 8, we are going to need this. This part is going to get 049 * removed once Smack depends on Android API 9 or higher. 050 * </p> 051 */ 052 private static StringTransformer idnaTransformer = new StringTransformer() { 053 @Override 054 public String transform(String string) { 055 return string; 056 } 057 }; 058 059 /** 060 * Set the DNS resolver that should be used to perform DNS lookups. 061 * 062 * @param resolver 063 */ 064 public static void setDNSResolver(DNSResolver resolver) { 065 dnsResolver = resolver; 066 } 067 068 /** 069 * Returns the current DNS resolved used to perform DNS lookups. 070 * 071 * @return the active DNSResolver 072 */ 073 public static DNSResolver getDNSResolver() { 074 return dnsResolver; 075 } 076 077 078 /** 079 * Set the IDNA (Internationalizing Domain Names in Applications, RFC 3490) transformer. 080 * <p> 081 * You usually want to wrap 'java.net.IDN.toASCII()' into a StringTransformer here. 082 * </p> 083 * @param idnaTransformer 084 */ 085 public static void setIdnaTransformer(StringTransformer idnaTransformer) { 086 if (idnaTransformer == null) { 087 throw new NullPointerException(); 088 } 089 DNSUtil.idnaTransformer = idnaTransformer; 090 } 091 092 private static enum DomainType { 093 Server, 094 Client, 095 ; 096 } 097 098 /** 099 * Returns a list of HostAddresses under which the specified XMPP server can be reached at for client-to-server 100 * communication. A DNS lookup for a SRV record in the form "_xmpp-client._tcp.example.com" is attempted, according 101 * to section 3.2.1 of RFC 6120. If that lookup fails, it's assumed that the XMPP server lives at the host resolved 102 * by a DNS lookup at the specified domain on the default port of 5222. 103 * <p> 104 * As an example, a lookup for "example.com" may return "im.example.com:5269". 105 * </p> 106 * 107 * @param domain the domain. 108 * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve. 109 * @return List of HostAddress, which encompasses the hostname and port that the 110 * XMPP server can be reached at for the specified domain. 111 */ 112 public static List<HostAddress> resolveXMPPDomain(String domain, List<HostAddress> failedAddresses) { 113 domain = idnaTransformer.transform(domain); 114 if (dnsResolver == null) { 115 LOGGER.warning("No DNS Resolver active in Smack, will be unable to perform DNS SRV lookups"); 116 List<HostAddress> addresses = new ArrayList<HostAddress>(1); 117 addresses.add(new HostAddress(domain, 5222)); 118 return addresses; 119 } 120 return resolveDomain(domain, DomainType.Client, failedAddresses); 121 } 122 123 /** 124 * Returns a list of HostAddresses under which the specified XMPP server can be reached at for server-to-server 125 * communication. A DNS lookup for a SRV record in the form "_xmpp-server._tcp.example.com" is attempted, according 126 * to section 3.2.1 of RFC 6120. If that lookup fails , it's assumed that the XMPP server lives at the host resolved 127 * by a DNS lookup at the specified domain on the default port of 5269. 128 * <p> 129 * As an example, a lookup for "example.com" may return "im.example.com:5269". 130 * </p> 131 * 132 * @param domain the domain. 133 * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve. 134 * @return List of HostAddress, which encompasses the hostname and port that the 135 * XMPP server can be reached at for the specified domain. 136 */ 137 public static List<HostAddress> resolveXMPPServerDomain(String domain, List<HostAddress> failedAddresses) { 138 domain = idnaTransformer.transform(domain); 139 if (dnsResolver == null) { 140 LOGGER.warning("No DNS Resolver active in Smack, will be unable to perform DNS SRV lookups"); 141 List<HostAddress> addresses = new ArrayList<HostAddress>(1); 142 addresses.add(new HostAddress(domain, 5269)); 143 return addresses; 144 } 145 return resolveDomain(domain, DomainType.Server, failedAddresses); 146 } 147 148 /** 149 * 150 * @param domain the domain. 151 * @param domainType the XMPP domain type, server or client. 152 * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve. 153 * @return a list of resolver host addresses for this domain. 154 */ 155 private static List<HostAddress> resolveDomain(String domain, DomainType domainType, List<HostAddress> failedAddresses) { 156 List<HostAddress> addresses = new ArrayList<HostAddress>(); 157 158 // Step one: Do SRV lookups 159 String srvDomain; 160 switch (domainType) { 161 case Server: 162 srvDomain = "_xmpp-server._tcp." + domain; 163 break; 164 case Client: 165 srvDomain = "_xmpp-client._tcp." + domain; 166 break; 167 default: 168 throw new AssertionError(); 169 } 170 try { 171 List<SRVRecord> srvRecords = dnsResolver.lookupSRVRecords(srvDomain); 172 if (LOGGER.isLoggable(Level.FINE)) { 173 String logMessage = "Resolved SRV RR for " + srvDomain + ":"; 174 for (SRVRecord r : srvRecords) 175 logMessage += " " + r; 176 LOGGER.fine(logMessage); 177 } 178 List<HostAddress> sortedRecords = sortSRVRecords(srvRecords); 179 addresses.addAll(sortedRecords); 180 } 181 catch (Exception e) { 182 LOGGER.log(Level.WARNING, "Exception while resovling SRV records for " + domain 183 + ". Consider adding '_xmpp-(server|client)._tcp' DNS SRV Records", e); 184 if (failedAddresses != null) { 185 HostAddress failedHostAddress = new HostAddress(srvDomain); 186 failedHostAddress.setException(e); 187 failedAddresses.add(failedHostAddress); 188 } 189 } 190 191 // Step two: Add the hostname to the end of the list 192 addresses.add(new HostAddress(domain)); 193 194 return addresses; 195 } 196 197 /** 198 * Sort a given list of SRVRecords as described in RFC 2782 199 * Note that we follow the RFC with one exception. In a group of the same priority, only the first entry 200 * is calculated by random. The others are ore simply ordered by their priority. 201 * 202 * @param records 203 * @return the list of resolved HostAddresses 204 */ 205 private static List<HostAddress> sortSRVRecords(List<SRVRecord> records) { 206 // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "." 207 // (the root domain), abort." 208 if (records.size() == 1 && records.get(0).getFQDN().equals(".")) 209 return Collections.emptyList(); 210 211 // sorting the records improves the performance of the bisection later 212 Collections.sort(records); 213 214 // create the priority buckets 215 SortedMap<Integer, List<SRVRecord>> buckets = new TreeMap<Integer, List<SRVRecord>>(); 216 for (SRVRecord r : records) { 217 Integer priority = r.getPriority(); 218 List<SRVRecord> bucket = buckets.get(priority); 219 // create the list of SRVRecords if it doesn't exist 220 if (bucket == null) { 221 bucket = new LinkedList<SRVRecord>(); 222 buckets.put(priority, bucket); 223 } 224 bucket.add(r); 225 } 226 227 List<HostAddress> res = new ArrayList<HostAddress>(records.size()); 228 229 for (Integer priority : buckets.keySet()) { 230 List<SRVRecord> bucket = buckets.get(priority); 231 int bucketSize; 232 while ((bucketSize = bucket.size()) > 0) { 233 int[] totals = new int[bucket.size()]; 234 int running_total = 0; 235 int count = 0; 236 int zeroWeight = 1; 237 238 for (SRVRecord r : bucket) { 239 if (r.getWeight() > 0) 240 zeroWeight = 0; 241 } 242 243 for (SRVRecord r : bucket) { 244 running_total += (r.getWeight() + zeroWeight); 245 totals[count] = running_total; 246 count++; 247 } 248 int selectedPos; 249 if (running_total == 0) { 250 // If running total is 0, then all weights in this priority 251 // group are 0. So we simply select one of the weights randomly 252 // as the other 'normal' algorithm is unable to handle this case 253 selectedPos = (int) (Math.random() * bucketSize); 254 } else { 255 double rnd = Math.random() * running_total; 256 selectedPos = bisect(totals, rnd); 257 } 258 // add the SRVRecord that was randomly chosen on it's weight 259 // to the start of the result list 260 SRVRecord chosenSRVRecord = bucket.remove(selectedPos); 261 res.add(chosenSRVRecord); 262 } 263 } 264 265 return res; 266 } 267 268 // TODO this is not yet really bisection just a stupid linear search 269 private static int bisect(int[] array, double value) { 270 int pos = 0; 271 for (int element : array) { 272 if (value < element) 273 break; 274 pos++; 275 } 276 return pos; 277 } 278 279}