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 089 * reached at for client-to-server communication. A DNS lookup for a SRV 090 * record in the form "_xmpp-client._tcp.example.com" is attempted, according 091 * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form 092 * of "_jabber._tcp.example.com" is attempted since servers that implement an 093 * older version of the protocol may be listed using that notation. If that 094 * lookup fails as well, it's assumed that the XMPP server lives at the 095 * host resolved by a DNS lookup at the specified domain on the default port 096 * of 5222.<p> 097 * 098 * As an example, a lookup for "example.com" may return "im.example.com:5269". 099 * 100 * @param domain the domain. 101 * @return List of HostAddress, which encompasses the hostname and port that the 102 * XMPP server can be reached at for the specified domain. 103 * @throws Exception 104 */ 105 public static List<HostAddress> resolveXMPPDomain(final String domain) throws Exception { 106 if (dnsResolver == null) { 107 List<HostAddress> addresses = new ArrayList<HostAddress>(1); 108 addresses.add(new HostAddress(domain, 5222)); 109 return addresses; 110 } 111 return resolveDomain(domain, 'c'); 112 } 113 114 /** 115 * Returns a list of HostAddresses under which the specified XMPP server can be 116 * reached at for server-to-server communication. A DNS lookup for a SRV 117 * record in the form "_xmpp-server._tcp.example.com" is attempted, according 118 * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form 119 * of "_jabber._tcp.example.com" is attempted since servers that implement an 120 * older version of the protocol may be listed using that notation. If that 121 * lookup fails as well, it's assumed that the XMPP server lives at the 122 * host resolved by a DNS lookup at the specified domain on the default port 123 * of 5269.<p> 124 * 125 * As an example, a lookup for "example.com" may return "im.example.com:5269". 126 * 127 * @param domain the domain. 128 * @return List of HostAddress, which encompasses the hostname and port that the 129 * XMPP server can be reached at for the specified domain. 130 * @throws Exception 131 */ 132 public static List<HostAddress> resolveXMPPServerDomain(final String domain) throws Exception { 133 if (dnsResolver == null) { 134 List<HostAddress> addresses = new ArrayList<HostAddress>(1); 135 addresses.add(new HostAddress(domain, 5269)); 136 return addresses; 137 } 138 return resolveDomain(domain, 's'); 139 } 140 141 private static List<HostAddress> resolveDomain(String domain, char keyPrefix) throws Exception { 142 List<HostAddress> addresses = new ArrayList<HostAddress>(); 143 144 // Step one: Do SRV lookups 145 String srvDomain; 146 if (keyPrefix == 's') { 147 srvDomain = "_xmpp-server._tcp." + domain; 148 } else if (keyPrefix == 'c') { 149 srvDomain = "_xmpp-client._tcp." + domain; 150 } else { 151 srvDomain = domain; 152 } 153 List<SRVRecord> srvRecords = dnsResolver.lookupSRVRecords(srvDomain); 154 if (LOGGER.isLoggable(Level.FINE)) { 155 String logMessage = "Resolved SRV RR for " + srvDomain + ":"; 156 for (SRVRecord r : srvRecords) 157 logMessage += " " + r; 158 LOGGER.fine(logMessage); 159 } 160 List<HostAddress> sortedRecords = sortSRVRecords(srvRecords); 161 addresses.addAll(sortedRecords); 162 163 // Step two: Add the hostname to the end of the list 164 addresses.add(new HostAddress(domain)); 165 166 return addresses; 167 } 168 169 /** 170 * Sort a given list of SRVRecords as described in RFC 2782 171 * Note that we follow the RFC with one exception. In a group of the same priority, only the first entry 172 * is calculated by random. The others are ore simply ordered by their priority. 173 * 174 * @param records 175 * @return the list of resolved HostAddresses 176 */ 177 private static List<HostAddress> sortSRVRecords(List<SRVRecord> records) { 178 // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "." 179 // (the root domain), abort." 180 if (records.size() == 1 && records.get(0).getFQDN().equals(".")) 181 return Collections.emptyList(); 182 183 // sorting the records improves the performance of the bisection later 184 Collections.sort(records); 185 186 // create the priority buckets 187 SortedMap<Integer, List<SRVRecord>> buckets = new TreeMap<Integer, List<SRVRecord>>(); 188 for (SRVRecord r : records) { 189 Integer priority = r.getPriority(); 190 List<SRVRecord> bucket = buckets.get(priority); 191 // create the list of SRVRecords if it doesn't exist 192 if (bucket == null) { 193 bucket = new LinkedList<SRVRecord>(); 194 buckets.put(priority, bucket); 195 } 196 bucket.add(r); 197 } 198 199 List<HostAddress> res = new ArrayList<HostAddress>(records.size()); 200 201 for (Integer priority : buckets.keySet()) { 202 List<SRVRecord> bucket = buckets.get(priority); 203 int bucketSize; 204 while ((bucketSize = bucket.size()) > 0) { 205 int[] totals = new int[bucket.size()]; 206 int running_total = 0; 207 int count = 0; 208 int zeroWeight = 1; 209 210 for (SRVRecord r : bucket) { 211 if (r.getWeight() > 0) 212 zeroWeight = 0; 213 } 214 215 for (SRVRecord r : bucket) { 216 running_total += (r.getWeight() + zeroWeight); 217 totals[count] = running_total; 218 count++; 219 } 220 int selectedPos; 221 if (running_total == 0) { 222 // If running total is 0, then all weights in this priority 223 // group are 0. So we simply select one of the weights randomly 224 // as the other 'normal' algorithm is unable to handle this case 225 selectedPos = (int) (Math.random() * bucketSize); 226 } else { 227 double rnd = Math.random() * running_total; 228 selectedPos = bisect(totals, rnd); 229 } 230 // add the SRVRecord that was randomly chosen on it's weight 231 // to the start of the result list 232 SRVRecord chosenSRVRecord = bucket.remove(selectedPos); 233 res.add(chosenSRVRecord); 234 } 235 } 236 237 return res; 238 } 239 240 // TODO this is not yet really bisection just a stupid linear search 241 private static int bisect(int[] array, double value) { 242 int pos = 0; 243 for (int element : array) { 244 if (value < element) 245 break; 246 pos++; 247 } 248 return pos; 249 } 250}