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