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}