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}