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