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