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